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/Base64_examples/decode_to_file.py b/Base64_examples/decode_to_file.py index 06d4ba375..a2ce75c7e 100644 --- a/Base64_examples/decode_to_file.py +++ b/Base64_examples/decode_to_file.py @@ -7,7 +7,7 @@ from base64 import b64decode -def decode_base64_to_file(file_name: str, text_base64: str): +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) diff --git a/Base64_examples/gui_base64.py b/Base64_examples/gui_base64.py index 70afbfd9e..ba49bbf97 100644 --- a/Base64_examples/gui_base64.py +++ b/Base64_examples/gui_base64.py @@ -24,7 +24,7 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -136,7 +136,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]) @@ -205,7 +205,7 @@ def __init__(self): self.setLayout(layout) - def show_detail_error_massage(self): + def show_detail_error_massage(self) -> None: message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() @@ -217,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() @@ -256,7 +256,7 @@ def input_text_changed(self): 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" diff --git a/CONTACT__examples/create_and_fill_database_from_dictionary.py b/CONTACT__examples/create_and_fill_database_from_dictionary.py index fc566ae27..4537d3050 100644 --- a/CONTACT__examples/create_and_fill_database_from_dictionary.py +++ b/CONTACT__examples/create_and_fill_database_from_dictionary.py @@ -120,7 +120,7 @@ def create_connect(): def create_table( table_name: str, sql_table: str, sql_table_data_rows: str, drop_table=False -): +) -> None: # Создание таблицы connect = create_connect() try: diff --git a/Callable modules/print_this.py b/Callable modules/print_this.py index 507c4999f..6f8f95e67 100644 --- a/Callable modules/print_this.py +++ b/Callable modules/print_this.py @@ -9,7 +9,7 @@ # SOURCE: https://stackoverflow.com/a/1060872/5909792 class mod_call(object): - def __call__(self, text): + def __call__(self, text) -> None: print(text) diff --git a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py index 4d9c6ba97..cb60a4485 100644 --- a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py +++ b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py @@ -7,7 +7,7 @@ import os -def main(namespace): +def main(namespace) -> None: # @param namespace argparse.Namespace Содержит переданные в аргументах объекты. indent = " " * 8 @@ -22,7 +22,7 @@ def main(namespace): ) -def create_parser(): +def create_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="CreateNoteXml", description="Cкрипт генерирует xml-файл программы NotesManager.", @@ -35,10 +35,11 @@ def create_parser(): 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() diff --git a/Crypto Git Repository/api.py b/Crypto Git Repository/api.py index f02684c0f..bbd03226b 100644 --- a/Crypto Git Repository/api.py +++ b/Crypto Git Repository/api.py @@ -33,7 +33,7 @@ def get_repo(): repo = get_repo() -def print_log(reverse=False): +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)}]:") @@ -44,27 +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/DNS price tracking/db.py b/DNS price tracking/db.py index 8bb6adc59..91a0635fe 100644 --- a/DNS price tracking/db.py +++ b/DNS price tracking/db.py @@ -16,7 +16,7 @@ DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / "tracked_products.sqlite") -def db_create_backup(backup_dir="backup"): +def db_create_backup(backup_dir="backup") -> None: os.makedirs(backup_dir, exist_ok=True) file_name = str(dt.datetime.today().date()) + ".sqlite" @@ -67,12 +67,12 @@ def get_last_price_technopoint(self, actual_price=True): return last_price.value_technopoint - def append_price(self, value_dns, 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}, " @@ -91,7 +91,7 @@ class Price(BaseModel): class Meta: indexes = ((("product_id", "date", "value_dns", "value_technopoint"), True),) - def __str__(self): + def __str__(self) -> str: return ( f"Price(value_dns={self.value_dns}, " f"value_technopoint={self.value_technopoint}, " 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 1c3138000..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" @@ -63,7 +63,7 @@ def fix_command(text): if __name__ == "__main__": # SHOW RESULT - def check(text): + def check(text) -> None: format_text = "{:<%s} -> {}" % (len(max(ALL_COMMANDS, key=len)) + 2) command = fix_command(text) @@ -95,8 +95,8 @@ def check(text): # # 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, f'Expected: "{expected}", get: "{command}"' diff --git a/Decorators__examples/append_handlers.py b/Decorators__examples/append_handlers.py index d95bbe4ce..c44b19bfd 100644 --- a/Decorators__examples/append_handlers.py +++ b/Decorators__examples/append_handlers.py @@ -8,7 +8,7 @@ class Collector: - def __init__(self): + def __init__(self) -> None: self.handlers = [] self.handlers_by_name = defaultdict(list) @@ -26,7 +26,7 @@ def decorator(func): @collector.add(name="test") -def hello_world(end="!"): +def hello_world(end="!") -> None: print("hello world" + end) @@ -41,7 +41,7 @@ def hello_world(end="!"): @collector.add(name="this it say_hello!") -def say_hello(): +def say_hello() -> None: print("hello!") diff --git a/Decorators__examples/combine_decorators__with_args.py b/Decorators__examples/combine_decorators__with_args.py index 506c98594..6e71ff93a 100644 --- a/Decorators__examples/combine_decorators__with_args.py +++ b/Decorators__examples/combine_decorators__with_args.py @@ -18,7 +18,7 @@ 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)}" @@ -30,7 +30,7 @@ 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)}" @@ -53,7 +53,7 @@ def custom_tag( ): def actual_decorator(func): @functools.wraps(func) - def wrapped(*args, **kwargs): + def wrapped(*args, **kwargs) -> str: attrs = get_attrs_str(arguments) return f"<{name}{attrs}>{func(*args, **kwargs)}" diff --git a/Decorators__examples/decorator__args_as_funcs.py b/Decorators__examples/decorator__args_as_funcs.py index eb8df38e5..b6b27499f 100644 --- a/Decorators__examples/decorator__args_as_funcs.py +++ b/Decorators__examples/decorator__args_as_funcs.py @@ -5,7 +5,7 @@ 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,7 +41,7 @@ def wrapper(self, *args, **kwargs): # Декоратор возвращает обертку return wrapper - # Возаращаем сам декоратор + # Возвращаем сам декоратор return decorator @_call_before(lambda self: self.result.append("+" + "-" * 10 + "+")) 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/example_1.py b/Decorators__examples/example_1.py index 82c0e38a6..e7d123809 100644 --- a/Decorators__examples/example_1.py +++ b/Decorators__examples/example_1.py @@ -1,7 +1,7 @@ __author__ = "ipetrash" -def getprint(str="hello world!"): +def getprint(str="hello world!") -> None: print(str) @@ -16,7 +16,7 @@ def wrapper(*args, **kwargs): return wrapper -def predecor(w="W"): +def predecor(w="W") -> None: print(w, end=": ") @@ -29,7 +29,7 @@ def predecor(w="W"): def rgb2hex(get_rgb_func): - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs) -> str: r, g, b = get_rgb_func(*args, **kwargs) return f"#{r:02x}{g:02x}{b:02x}" @@ -37,7 +37,7 @@ def wrapper(*args, **kwargs): class RGB: - def __init__(self): + def __init__(self) -> None: self._r = 0xFF self._g = 0xFF self._b = 0xFF @@ -45,7 +45,7 @@ def __init__(self): def getr(self): return self._r - def setr(self, r): + def setr(self, r) -> None: self._r = r r = property(getr, setr) @@ -53,7 +53,7 @@ def setr(self, r): def getg(self): return self._g - def setg(self, g): + def setg(self, g) -> None: self._g = g g = property(getg, setg) @@ -61,12 +61,12 @@ def setg(self, g): def getb(self): return self._b - def setb(self, b): + def setb(self, b) -> None: self._b = b b = property(getb, setb) - def setrgb(self, r, g, b): + def setrgb(self, r, g, b) -> None: self.r, self.g, self.b = r, g, b @rgb2hex @@ -82,7 +82,7 @@ def getrgb(self): @decor -def foo(a, b): +def foo(a, b) -> None: print(f"{a} ^ {b} = {(a ** b)}") 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 2c68b48ae..3654b2225 100644 --- a/Decorators__examples/memoize_class.py +++ b/Decorators__examples/memoize_class.py @@ -6,7 +6,7 @@ # 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/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 bbb34bd2e..70426ff29 100644 --- a/Decorators__examples/thread.py +++ b/Decorators__examples/thread.py @@ -9,7 +9,7 @@ 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() @@ -18,14 +18,14 @@ def wrapper(*args, **kwargs): if __name__ == "__main__": @thread - def _print_and_sleep(timeout=2): + def _print_and_sleep(timeout=2) -> None: print("start. print_and_sleep") time.sleep(timeout) print("finish. print_and_sleep") @thread - def _print_loop(name, max_num=10): + def _print_loop(name, max_num=10) -> None: print("start. _print_loop") i = 0 diff --git a/Decorators__examples/timer.py b/Decorators__examples/timer.py index aa8e5d92a..c441a32ad 100644 --- a/Decorators__examples/timer.py +++ b/Decorators__examples/timer.py @@ -19,7 +19,7 @@ def wrapper(*args, **kwargs): if __name__ == "__main__": @timer - def my_sleep(): + def my_sleep() -> None: print(123) time.sleep(0.3) print(456) diff --git a/EscapePyPromptLineString/main.py b/EscapePyPromptLineString/main.py index 57a403ee7..4555e837e 100644 --- a/EscapePyPromptLineString/main.py +++ b/EscapePyPromptLineString/main.py @@ -23,7 +23,7 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -36,7 +36,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("EscapePyPromptLineString") @@ -77,7 +77,7 @@ def __init__(self): self.setLayout(layout) - def show_detail_error_massage(self): + def show_detail_error_massage(self) -> None: message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() @@ -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() diff --git a/EscapeString/main.py b/EscapeString/main.py index d90a6f4dc..c6b7903dd 100644 --- a/EscapeString/main.py +++ b/EscapeString/main.py @@ -23,7 +23,7 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -47,7 +47,7 @@ class MainWindow(QWidget): ] TITLE = "EscapeString" - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle(self.TITLE) @@ -129,7 +129,7 @@ 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 @@ -161,7 +161,7 @@ def _escape(self, in_text: str, ignored="") -> str: return "".join(out_text) - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() diff --git a/Fibonacci number/6 ways.py b/Fibonacci number/6 ways.py index 4cef3fe92..25cd58937 100644 --- a/Fibonacci number/6 ways.py +++ b/Fibonacci number/6 ways.py @@ -66,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() diff --git a/Fibonacci number/FibonacciNumber.py b/Fibonacci number/FibonacciNumber.py index c9015d9c3..a164d783e 100644 --- a/Fibonacci number/FibonacciNumber.py +++ b/Fibonacci number/FibonacciNumber.py @@ -1,14 +1,14 @@ __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]) print(f) -def fibo_2(n): +def fibo_2(n) -> None: a, b = 0, 1 print(a, b, end=" ") for i in range(2, n + 1): diff --git a/HelloPython/HelloPython.py b/HelloPython/HelloPython.py index 45acc7ba5..388f750d8 100644 --- a/HelloPython/HelloPython.py +++ b/HelloPython/HelloPython.py @@ -5,7 +5,7 @@ from datetime import datetime, time -def main(namespace): +def main(namespace) -> None: args = namespace.parse_args() args.user = "Илья" if args.user: @@ -29,7 +29,7 @@ def main(namespace): print("Привет, Python!") -def create_parser(): +def create_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Hello World Example!") parser.add_argument("--user", type=str, help=" user name.") return parser 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/No choice line edit.py b/No choice line edit.py index b28356739..c8db94aac 100644 --- a/No choice line edit.py +++ b/No choice line edit.py @@ -14,7 +14,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("No choice") @@ -33,7 +33,7 @@ def __init__(self): self.line_edit_no_choice.setFocus() - def on_text_edited_no_choice(self, 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) diff --git a/ObjectWithArrayAccess.py b/ObjectWithArrayAccess.py index 0cdd90e4e..1886745e0 100644 --- a/ObjectWithArrayAccess.py +++ b/ObjectWithArrayAccess.py @@ -5,16 +5,16 @@ 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) diff --git a/OpenSSL_example/generate_selfsigned_cert.py b/OpenSSL_example/generate_selfsigned_cert.py index 8e9ee5663..c71bd75f5 100644 --- a/OpenSSL_example/generate_selfsigned_cert.py +++ b/OpenSSL_example/generate_selfsigned_cert.py @@ -22,7 +22,7 @@ def cert_gen( validity_end_in_seconds=10 * 365 * 24 * 60 * 60, key_file="key.pem", cert_file="cert.pem", -): +) -> None: # Create a key pair k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, 4096) diff --git a/OpenSSL_example/p12_to_pem.py b/OpenSSL_example/p12_to_pem.py index d0d53a4ac..8c419fd66 100644 --- a/OpenSSL_example/p12_to_pem.py +++ b/OpenSSL_example/p12_to_pem.py @@ -8,7 +8,7 @@ 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.""" 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 63eb494c3..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 @@ -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 c02bb2f7a..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 @@ -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/gist_history_to_sqlite_db.py b/PyGithub_examples/gist_history_to_sqlite_db.py index 06b68f552..8cd7ca839 100644 --- a/PyGithub_examples/gist_history_to_sqlite_db.py +++ b/PyGithub_examples/gist_history_to_sqlite_db.py @@ -12,7 +12,7 @@ def create_connect(): return sqlite3.connect("gist_commits.sqlite") -def init_db(): +def init_db() -> None: # Создание базы и таблицы with create_connect() as connect: connect.execute( diff --git a/PyOpenGLExample/checkerboard.py b/PyOpenGLExample/checkerboard.py index 5d4ae28e6..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) diff --git a/PyOpenGLExample/gingerbread.py b/PyOpenGLExample/gingerbread.py index da64a3b00..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) diff --git a/PyOpenGLExample/helloworld.py b/PyOpenGLExample/helloworld.py index 227086e07..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) diff --git a/PyOpenGLExample/maze.py b/PyOpenGLExample/maze.py index 039ffe3a0..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 @@ -155,7 +155,7 @@ def __init__(self, dimx, dimy): # Push it back on the list since this is our next node cell_list.append(conn) - def draw(self): + def draw(self) -> None: """Draws the field""" glBegin(GL_LINES) for i in range(0, self.numx * self.numy): @@ -184,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) @@ -193,7 +193,7 @@ 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() diff --git a/PyOpenGLExample/mouse.py b/PyOpenGLExample/mouse.py index 3903dce15..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) diff --git a/PyOpenGLExample/print_text.py b/PyOpenGLExample/print_text.py index 4d58f78c7..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,12 +43,12 @@ 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() @@ -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,7 +80,7 @@ def glut_print(x, y, font, text, r, g, b, a): glDisable(GL_BLEND) -def keyPressed(*args): +def keyPressed(*args) -> None: ESCAPE = b"\x1b" if args[0] == ESCAPE: sys.exit() diff --git a/PyOpenGLExample/reshape.py b/PyOpenGLExample/reshape.py index 360c28728..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) diff --git a/PyOpenGLExample/rosette.py b/PyOpenGLExample/rosette.py index 561e50f84..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 = [] diff --git a/PyOpenGLExample/scatter.py b/PyOpenGLExample/scatter.py index 82be48d3b..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) @@ -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) diff --git a/PyOpenGLExample/sierpinski.py b/PyOpenGLExample/sierpinski.py index f70bfb32d..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) diff --git a/PyOpenGLExample/squares.py b/PyOpenGLExample/squares.py index 64dd72e86..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,7 +24,7 @@ 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 = random.randint(0, 25) / 25.0 diff --git a/PyOpenGLExample/turtle.py b/PyOpenGLExample/turtle.py index a1deb6187..4b8e94969 100644 --- a/PyOpenGLExample/turtle.py +++ b/PyOpenGLExample/turtle.py @@ -23,7 +23,7 @@ angle = 0.0 -def reset(): +def reset() -> None: """Reset the position to the origin""" global curX global curY @@ -34,19 +34,19 @@ def reset(): angle = 0.0 -def turnTo(deg): +def turnTo(deg) -> None: """Turn to a certain angle""" global angle angle = deg -def turn(deg): +def turn(deg) -> None: """Turn a certain number of degrees""" global angle angle += deg -def forw(len, visible): +def forw(len, visible) -> None: """Move forward over a certain distance""" global curX global curY @@ -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() diff --git a/PyOpenGLExample/wireframe.py b/PyOpenGLExample/wireframe.py index 47523d88b..1d16c92d7 100644 --- a/PyOpenGLExample/wireframe.py +++ b/PyOpenGLExample/wireframe.py @@ -12,7 +12,7 @@ """ -def axis(length): +def axis(length) -> None: """Draws an axis (basicly a line with a cone on top)""" glPushMatrix() glBegin(GL_LINES) @@ -24,7 +24,7 @@ def axis(length): glPopMatrix() -def three_axis(length): +def three_axis(length) -> None: """Draws an X, Y and Z-axis""" glPushMatrix() @@ -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) diff --git a/Serialize with pickle/SerializeWithPickle.py b/Serialize with pickle/SerializeWithPickle.py index e799b9918..2ad97101d 100644 --- a/Serialize with pickle/SerializeWithPickle.py +++ b/Serialize with pickle/SerializeWithPickle.py @@ -48,20 +48,20 @@ class Monster: """Monster class!""" - def __init__(self, health, power, name, level): + 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): + 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): + def say(self) -> None: print("I'm %s" % self.name) zombi = Monster(name="Zombi", health=100, power=10, level=2) diff --git a/Sorting algorithm list/comparison_of_sorting.py b/Sorting algorithm list/comparison_of_sorting.py index 2d9757002..afb189f32 100644 --- a/Sorting algorithm list/comparison_of_sorting.py +++ b/Sorting algorithm list/comparison_of_sorting.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) @@ -48,7 +48,7 @@ def localtrace(self, frame, why, arg): raise Kill() return self.localtrace - def kill(self): + def kill(self) -> None: self.killed = True @@ -75,9 +75,10 @@ def the_wrapper_around_the_original_function(*args, **kwargs): if __name__ == "__main__": import random - import time import sorts + from timeit import default_timer + items = list(range(10**3)) random.shuffle(items) @@ -87,11 +88,11 @@ def the_wrapper_around_the_original_function(*args, **kwargs): 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() @@ -101,7 +102,7 @@ def run(): except Exception as e: 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)) diff --git a/Sorting algorithm list/sorts/bogosort.py b/Sorting algorithm list/sorts/bogosort.py index 6d302b43f..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): diff --git a/Sorting algorithm list/sorts/heap_sort.py b/Sorting algorithm list/sorts/heap_sort.py index d1eeb2356..869ed18fb 100644 --- a/Sorting algorithm list/sorts/heap_sort.py +++ b/Sorting algorithm list/sorts/heap_sort.py @@ -13,7 +13,7 @@ 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 diff --git a/Sorting/sorting.py b/Sorting/sorting.py index 330c7038c..e441a5323 100644 --- a/Sorting/sorting.py +++ b/Sorting/sorting.py @@ -24,11 +24,11 @@ class Student: - def __init__(self, name, age): + def __init__(self, name, age) -> None: self.name = name self.age = age - def __repr__(self): + def __repr__(self) -> str: return "%s (%d)" % (self.name, self.age) diff --git a/Timer/pyside_qtimer.py b/Timer/pyside_qtimer.py index 1d02eeb79..4d6640a2f 100644 --- a/Timer/pyside_qtimer.py +++ b/Timer/pyside_qtimer.py @@ -8,7 +8,7 @@ from PySide.QtCore import * -def say(): +def say() -> None: print("say!") diff --git a/WrapperMap__work_with_dict_through_atts.py b/WrapperMap__work_with_dict_through_atts.py index 8cc9b7cfe..316813bf1 100644 --- a/WrapperMap__work_with_dict_through_atts.py +++ b/WrapperMap__work_with_dict_through_atts.py @@ -5,7 +5,7 @@ class WrapperMap: - def __init__(self, d: dict): + def __init__(self, d: dict) -> None: self.d = d def get_value(self): @@ -18,7 +18,7 @@ def __getattr__(self, item: str): return value - def __repr__(self): + def __repr__(self) -> str: return repr(self.d) 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 9c53aa684..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 @@ -12,13 +12,13 @@ class Dog: - def __init__(self, name): + def __init__(self, name) -> None: self.name = name self.type = "Animal" self.paws = 4 self.has_tail = True - def __repr__(self): + def __repr__(self) -> str: return f'' diff --git a/XML/XML_to_dict__xmltodict__examples/streaming_mode.py b/XML/XML_to_dict__xmltodict__examples/streaming_mode.py index c7c8086e4..ead6df188 100644 --- a/XML/XML_to_dict__xmltodict__examples/streaming_mode.py +++ b/XML/XML_to_dict__xmltodict__examples/streaming_mode.py @@ -11,7 +11,7 @@ import xmltodict -def handle(path, 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 d9be22934..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 @@ -18,7 +18,7 @@ url = "http://discogs-data.s3-us-west-2.amazonaws.com/data/2018/discogs_20180201_artists.xml.gz" -def handle_artist(_, artist): +def handle_artist(_, artist) -> bool: print(artist["name"]) return True 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/sax_FIAS_with_progress__HOUSEGUID.py b/XML/sax_FIAS_with_progress__HOUSEGUID.py index ddcb1ba70..39f211183 100644 --- a/XML/sax_FIAS_with_progress__HOUSEGUID.py +++ b/XML/sax_FIAS_with_progress__HOUSEGUID.py @@ -13,10 +13,10 @@ 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): + def startElement(self, name, attrs) -> None: if "HOUSEGUID" in attrs: all_house_guid.add(attrs["HOUSEGUID"]) diff --git a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py index b4f98434c..71e03517a 100644 --- a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py +++ b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py @@ -13,11 +13,11 @@ 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): + def startElement(self, name, attrs) -> None: if "HOUSEGUID" in attrs: guid = attrs["HOUSEGUID"] if self.number > 0: 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 48891d48b..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 @@ -9,19 +9,19 @@ 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.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("[") - def startElement(self, name, attrs): + def startElement(self, name, attrs) -> None: if "HOUSEGUID" in attrs: guid = attrs["HOUSEGUID"] @@ -33,7 +33,7 @@ def startElement(self, name, attrs): next(self.it) - def endDocument(self): + def endDocument(self) -> None: self.f.write("]") self.f.close() diff --git a/XML/xml.etree.ElementTree__examples/pretty_print.py b/XML/xml.etree.ElementTree__examples/pretty_print.py index ff36c786a..93836b547 100644 --- a/XML/xml.etree.ElementTree__examples/pretty_print.py +++ b/XML/xml.etree.ElementTree__examples/pretty_print.py @@ -10,7 +10,7 @@ # SOURCE: http://effbot.org/zone/element-lib.htm#prettyprint -def indent(elem, level=0): +def indent(elem, level=0) -> None: i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): 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 f9f24ae66..0fa8d4d0d 100644 --- a/XML/xml.parsers.expat__examples__like_sax/hello_world.py +++ b/XML/xml.parsers.expat__examples__like_sax/hello_world.py @@ -11,15 +11,15 @@ # 3 handler functions -def on_start_element(name, attrs): +def on_start_element(name, attrs) -> None: print("Start element:", name, attrs) -def on_end_element(name): +def on_end_element(name) -> None: print("End element:", name) -def on_char_data(data): +def on_char_data(data) -> None: print("Character data:", repr(data)) diff --git a/XML/xml_replace_comments/xml_replace_comments.py b/XML/xml_replace_comments/xml_replace_comments.py index a08375ee6..868ee74e4 100644 --- a/XML/xml_replace_comments/xml_replace_comments.py +++ b/XML/xml_replace_comments/xml_replace_comments.py @@ -9,7 +9,7 @@ from lxml import etree -def replace(file_name, save_file_name): +def replace(file_name, save_file_name) -> None: with open(file_name, encoding="utf8") as f: text = f.read() 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 fd4cdaeb2..86c64dacc 100644 --- a/_FOO_TEST_TEST/FOO_TEST_TEST.py +++ b/_FOO_TEST_TEST/FOO_TEST_TEST.py @@ -4,3 +4,885 @@ __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/job_compassplus/parse_jira_logged_time/common.py b/_FOO_TEST_TEST/config.py similarity index 82% rename from job_compassplus/parse_jira_logged_time/common.py rename to _FOO_TEST_TEST/config.py index 6c28083e4..07b3e6c3d 100644 --- a/job_compassplus/parse_jira_logged_time/common.py +++ b/_FOO_TEST_TEST/config.py @@ -8,4 +8,5 @@ DIR = Path(__file__).resolve().parent -ROOT_DIR = DIR.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/main.py b/_FOO_TEST_TEST/parse_web_outlook_calendar/main.py deleted file mode 100644 index 6e793436b..000000000 --- a/_FOO_TEST_TEST/parse_web_outlook_calendar/main.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = "ipetrash" - - -import time - -# pip install selenium -from selenium import webdriver -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(f'Title: "{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(f'Title: "{driver.title}"') - - driver.save_screenshot("before_click_on_calendar.png") - print(f'Title: "{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(f'Title: "{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(f'Title: "{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/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/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 8f074b831..b905dc183 100644 --- a/aiohttp__asyncio__examples/aiohttp_check_user_agent.py +++ b/aiohttp__asyncio__examples/aiohttp_check_user_agent.py @@ -10,7 +10,7 @@ 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: print("Status:", rs.status) diff --git a/aiohttp__asyncio__examples/aiohttp_client.py b/aiohttp__asyncio__examples/aiohttp_client.py index 07dd57d52..44ba6d470 100644 --- a/aiohttp__asyncio__examples/aiohttp_client.py +++ b/aiohttp__asyncio__examples/aiohttp_client.py @@ -10,7 +10,7 @@ import aiohttp -async def main(): +async def main() -> None: url = "https://python.org" async with aiohttp.ClientSession() as session: diff --git a/aiohttp__asyncio__examples/asyncio__hello_world.py b/aiohttp__asyncio__examples/asyncio__hello_world.py index 1c4032bd3..432a7777e 100644 --- a/aiohttp__asyncio__examples/asyncio__hello_world.py +++ b/aiohttp__asyncio__examples/asyncio__hello_world.py @@ -7,7 +7,7 @@ import asyncio -async def main(): +async def main() -> None: print("Hello ", end="") await asyncio.sleep(1) print("World!") diff --git a/aiohttp__asyncio__examples/concurrent_requests.py b/aiohttp__asyncio__examples/concurrent_requests.py index cfc4fe6ea..647a97859 100644 --- a/aiohttp__asyncio__examples/concurrent_requests.py +++ b/aiohttp__asyncio__examples/concurrent_requests.py @@ -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,7 +21,7 @@ async def fetch_page(url: str, idx: int): print(rs.content) -async def main(): +async def main() -> None: url = "https://python.org" urls = [url] * 100 diff --git a/aiohttp__asyncio__examples/hello_world.py b/aiohttp__asyncio__examples/hello_world.py index 40e951117..0a395a322 100644 --- a/aiohttp__asyncio__examples/hello_world.py +++ b/aiohttp__asyncio__examples/hello_world.py @@ -17,7 +17,7 @@ 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") print(html) diff --git a/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py b/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py index a432ebd34..508eefbc5 100644 --- a/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py +++ b/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py @@ -20,7 +20,7 @@ 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") root = etree.fromstring(xml_str) diff --git a/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py b/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py index f30eb2205..f6beffd36 100644 --- a/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py +++ b/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py @@ -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,7 +41,7 @@ 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", diff --git a/alpha2_to_country/main.py b/alpha2_to_country/main.py index 8cba527aa..c8ca59b42 100644 --- a/alpha2_to_country/main.py +++ b/alpha2_to_country/main.py @@ -18,7 +18,7 @@ ALPHA2_TO_COUNTRY = None -def init(): +def init() -> None: global ALPHA2_TO_COUNTRY if FILE_NAME_COUNTRY.exists(): diff --git a/api_clans.py b/api_clans.py index 66108962c..54ee5fbb6 100644 --- a/api_clans.py +++ b/api_clans.py @@ -14,7 +14,7 @@ class Api: # TODO: this 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 diff --git a/ascii_table__simple_pretty__format.py b/ascii_table__simple_pretty__format.py index 5bf24e712..afe0340f7 100644 --- a/ascii_table__simple_pretty__format.py +++ b/ascii_table__simple_pretty__format.py @@ -32,7 +32,7 @@ def pretty_table(data, cell_sep=" | ", header_separator=True, align=">") -> str: 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)) diff --git a/ascii_table__simple_pretty__ljust.py b/ascii_table__simple_pretty__ljust.py index dda60264d..b762934b4 100644 --- a/ascii_table__simple_pretty__ljust.py +++ b/ascii_table__simple_pretty__ljust.py @@ -31,7 +31,7 @@ def pretty_table(data, cell_sep=" | ", header_separator=True) -> str: 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)) diff --git a/ascii_table__simple_pretty__rjust.py b/ascii_table__simple_pretty__rjust.py index 2b3ce49a0..8abdf420d 100644 --- a/ascii_table__simple_pretty__rjust.py +++ b/ascii_table__simple_pretty__rjust.py @@ -31,7 +31,7 @@ def pretty_table(data, cell_sep=" | ", header_separator=True) -> str: 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)) diff --git a/autoclick_RDP_task_icon/main.py b/autoclick_RDP_task_icon/main.py index 02cee0956..d8caa22fe 100644 --- a/autoclick_RDP_task_icon/main.py +++ b/autoclick_RDP_task_icon/main.py @@ -42,7 +42,7 @@ def get_pos_rdp_task_icon() -> tuple[int, int] | None: pass -def run(): +def run() -> None: logging.info("") logging.info("Run") diff --git a/bin2str/gui.py b/bin2str/gui.py index f96c4e1a1..808c22891 100644 --- a/bin2str/gui.py +++ b/bin2str/gui.py @@ -24,7 +24,7 @@ from bin2str import bin2str, str2bin -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -37,7 +37,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("bin2str") @@ -65,13 +65,13 @@ def __init__(self): self.setLayout(layout) - def _bin2str(self): + def _bin2str(self) -> None: text = self.plain_text_bin.toPlainText() text = bin2str(text) self.plain_text_str.setPlainText(text) - def _str2bin(self): + def _str2bin(self) -> None: text = self.plain_text_str.toPlainText() text = str2bin(text) diff --git a/build_exe/pyinstaller_example/compress_exe/main.py b/build_exe/pyinstaller_example/compress_exe/main.py index 88ac109f1..f1e929469 100644 --- a/build_exe/pyinstaller_example/compress_exe/main.py +++ b/build_exe/pyinstaller_example/compress_exe/main.py @@ -17,7 +17,7 @@ button = QPushButton("Add") text_edit = QTextEdit() - def add_to_text(): + def add_to_text() -> None: text = line_edit.text() text_edit.append(text) diff --git a/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py b/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py index 97c3ea417..f504fb661 100644 --- a/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py +++ b/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py @@ -22,7 +22,7 @@ text_edit = QTextEdit() -def add_to_text(): +def add_to_text() -> None: text = line_edit.text() text_edit.append(text) diff --git a/build_exe/pyinstaller_example/gui/qt_pyside/main.py b/build_exe/pyinstaller_example/gui/qt_pyside/main.py index 88ac109f1..f1e929469 100644 --- a/build_exe/pyinstaller_example/gui/qt_pyside/main.py +++ b/build_exe/pyinstaller_example/gui/qt_pyside/main.py @@ -17,7 +17,7 @@ button = QPushButton("Add") text_edit = QTextEdit() - def add_to_text(): + def add_to_text() -> None: text = line_edit.text() text_edit.append(text) diff --git a/build_fake_image__build_exe/generator.py b/build_fake_image__build_exe/generator.py index f16052dc1..8b0eabda3 100644 --- a/build_fake_image__build_exe/generator.py +++ b/build_fake_image__build_exe/generator.py @@ -9,7 +9,7 @@ # 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): +def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None) -> None: img = Image.open(file_name) if icon_sizes: @@ -22,7 +22,7 @@ def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/aec64c1749d4f6f3176e3222c7e7c554f40c693f/generator_py_with_inner_image_with_open/main.py -def generate(file_name, inject_code: str): +def generate(file_name, inject_code: str) -> None: with open(file_name, "rb") as f: img_bytes = f.read() 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/codewars/parse_molecule__Molecule to atoms.py b/codewars/parse_molecule__Molecule to atoms.py index 18f928ef8..3484bc9eb 100644 --- a/codewars/parse_molecule__Molecule to atoms.py +++ b/codewars/parse_molecule__Molecule to atoms.py @@ -97,7 +97,7 @@ def parse_molecule(formula): if __name__ == "__main__": - def equals_atomically(obj1, obj2): + def equals_atomically(obj1, obj2) -> bool: if len(obj1) != len(obj2): return False for k in obj1: diff --git a/codingame/medium/Bender - Episode 1.py b/codingame/medium/Bender - Episode 1.py index 9b293e7e7..bd97edcc7 100644 --- a/codingame/medium/Bender - Episode 1.py +++ b/codingame/medium/Bender - Episode 1.py @@ -45,12 +45,12 @@ 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() # Соберем все объекты на карте в словарь, исключаются пустые места и стенки @@ -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,7 +99,7 @@ def _get_pos_j(self): pos_j = property(_get_pos_j, _set_pos_j) - def _set_pos(self, value): + def _set_pos(self, value) -> None: self.objects_map["@"] = value def _get_pos(self): @@ -120,13 +120,13 @@ def city_map(self): return map - def print_city_map(self): + def print_city_map(self) -> None: log() for row in self.city_map(): 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): diff --git a/collector_bash_im/collector_bash_im.py b/collector_bash_im/collector_bash_im.py index 1998bca75..eddb6ae3c 100644 --- a/collector_bash_im/collector_bash_im.py +++ b/collector_bash_im/collector_bash_im.py @@ -43,13 +43,13 @@ def get_logger(name, file="log.txt", encoding="utf8"): 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): + def __repr__(self) -> str: return ( "".format(len(self.text), **self.__dict__) 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 d0c526330..b03abd482 100644 --- a/concurrency_in_python__threading_processing/about_1__single_thread.py +++ b/concurrency_in_python__threading_processing/about_1__single_thread.py @@ -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 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 b2bdc2974..4e68c7285 100644 --- a/concurrency_in_python__threading_processing/about_2__multithreading.py +++ b/concurrency_in_python__threading_processing/about_2__multithreading.py @@ -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): diff --git a/concurrency_in_python__threading_processing/about_3__multiprocessing.py b/concurrency_in_python__threading_processing/about_3__multiprocessing.py index d2121c1b0..6e71b2722 100644 --- a/concurrency_in_python__threading_processing/about_3__multiprocessing.py +++ b/concurrency_in_python__threading_processing/about_3__multiprocessing.py @@ -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): 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 1b7dbfd35..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 @@ -14,11 +14,11 @@ 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)) 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 6f440e73d..7695daacc 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py @@ -7,7 +7,7 @@ from multiprocessing import Process, Manager -def f(users_data): +def f(users_data) -> None: users_data["users"][0]["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 bdc947530..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 @@ -7,7 +7,7 @@ import multiprocessing -def run(): +def run() -> None: import time i = 1 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 376ccd528..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 @@ -7,7 +7,7 @@ from multiprocessing import Process, Pipe -def f(con, name): +def f(con, name) -> None: con.send("Hello, " + name) con.close() 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 ba4474230..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 @@ -7,7 +7,7 @@ from multiprocessing import Process, Queue -def f(q, name): +def f(q, name) -> None: q.put("Hello, " + name) 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 56b47918a..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 @@ -7,7 +7,7 @@ from multiprocessing import Process -def f(name): +def f(name) -> None: print("Hello,", name) 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 61d1c752e..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 @@ -8,7 +8,7 @@ import time -def run(): +def run() -> None: print(current_process()) for i in "Hello World!": 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 68d040a7d..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 @@ -8,7 +8,7 @@ import time -def run(): +def run() -> None: print(current_process()) for i in "Hello World!": 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 1da85f550..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 @@ -9,12 +9,12 @@ from multiprocessing__Tkinter__in_other_process import go as go_tk -def create_qt(): +def create_qt() -> None: p = Process(target=go_qt, args=("Qt",)) p.start() -def create_tk(): +def create_tk() -> None: p = Process(target=go_tk, args=("Tk",)) p.start() 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 a8b24f1b8..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 @@ -7,7 +7,7 @@ from PyQt5.Qt import QApplication, Qt, QLabel -def go(name): +def go(name) -> None: app = QApplication([]) mw = QLabel() 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 cb49bec54..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 @@ -7,7 +7,7 @@ import tkinter as tk -def go(name): +def go(name) -> None: app = tk.Tk() app.minsize(150, 50) 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 081faf97b..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 @@ -12,19 +12,19 @@ from flask import Flask -def go(port: int): +def go(port: int) -> None: app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") - def index(): + def index() -> str: return f"Hello World! (port={port})" app.run(port=port) -def go_parser(urls): +def go_parser(urls) -> None: while True: for url in urls: try: 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 82077c0b0..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 @@ -8,14 +8,14 @@ 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()) -def f(name): +def f(name) -> None: info("function f") print("Hello,", name) 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 2354f06da..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 @@ -11,7 +11,7 @@ import time -def f(lock, i): +def f(lock, i) -> None: with lock: print("hello world", i) 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 48ddc315b..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 @@ -21,7 +21,7 @@ ] -def is_prime(n): +def is_prime(n) -> bool: if n < 2: return False if n == 2: @@ -36,7 +36,7 @@ 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: 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 272919efa..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 @@ -15,7 +15,7 @@ MAX_WORKERS = 5 -def run(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 f8473ff8f..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 @@ -15,7 +15,7 @@ MAX_WORKERS = 5 -def run(name): +def run(name) -> str: time.sleep(randint(1, 4)) return f"name: {name}" 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 1ce431733..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 @@ -41,7 +41,7 @@ class AtomicCounter: 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() @@ -66,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 b8e1c9419..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 @@ -8,7 +8,7 @@ import time -def run(): +def run() -> None: i = 1 # Бесконечный цикл 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 d9df962b1..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 @@ -37,7 +37,7 @@ # ------- VERSUS ------- # -def go(count=1): +def go(count=1) -> None: t = time.clock() pool = ThreadPool(count) results = pool.map(urlopen, urls) 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 488e0fa0c..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 @@ -12,7 +12,7 @@ @app.route("/") -def index(): +def index() -> str: return f"Current thread: {threading.current_thread()}" 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 b8ca68bfb..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 @@ -12,7 +12,7 @@ @app.route("/") -def index(): +def index() -> str: return f"Current thread: {threading.current_thread()}" 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 43eb3f4fd..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 @@ -8,7 +8,7 @@ import time -def run(): +def run() -> None: print(threading.current_thread()) for i in "Hello World!": print(i) 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 1d58ddeec..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 @@ -8,7 +8,7 @@ import time -def run(): +def run() -> None: print(threading.current_thread()) for i in "Hello World!": print(i) 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 b76d3b282..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 @@ -8,7 +8,7 @@ import time -def run(name="main", sleep_seconds=None): +def run(name="main", sleep_seconds=None) -> None: if sleep_seconds: time.sleep(sleep_seconds) 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 52a71aac2..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 @@ -13,14 +13,14 @@ lock = threading.Lock() -def inc(*args): +def inc(*args) -> None: global number DATA["number"] += 1 number += 1 -def inc_lock(*args): +def inc_lock(*args) -> None: global number with lock: 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 86014f261..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 @@ -8,13 +8,13 @@ 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 0eae78b3b..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 @@ -10,7 +10,7 @@ from flask import Flask -def run(port: int = 80): +def run(port: int = 80) -> None: app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) 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 f1ec750a9..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 @@ -9,10 +9,10 @@ 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) # Написать пользователю @@ -26,7 +26,7 @@ def post_msg(self): Thread(target=user_2.post_msg).start() -def foo(name): +def foo(name) -> None: time.sleep(5) # Написать пользователю diff --git a/console_change_data_in_line.py b/console_change_data_in_line.py index 8b6ff06cb..2e79600ef 100644 --- a/console_change_data_in_line.py +++ b/console_change_data_in_line.py @@ -8,7 +8,7 @@ import time -def f1(n): +def f1(n) -> None: last_num_chars = 0 write, flush = sys.stdout.write, sys.stdout.flush @@ -23,7 +23,7 @@ def f1(n): print() -def f2(n): +def f2(n) -> None: last_num_chars = 0 write, flush = sys.stdout.write, sys.stdout.flush 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 afdc6c4d4..db0959749 100644 --- a/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py +++ b/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py @@ -11,7 +11,7 @@ with StringIO() as f, redirect_stdout(f): print("Hello ", end="") - def foo(): + def foo() -> None: print("World") foo() diff --git a/convert_image_to_ico/main.py b/convert_image_to_ico/main.py index 1f4f6442c..6fc355776 100644 --- a/convert_image_to_ico/main.py +++ b/convert_image_to_ico/main.py @@ -7,7 +7,7 @@ from PIL import Image -def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): +def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None) -> None: img = Image.open(file_name) if icon_sizes: diff --git a/copy2clipboard.py b/copy2clipboard.py index 707e468c3..95f5988f2 100644 --- a/copy2clipboard.py +++ b/copy2clipboard.py @@ -15,7 +15,7 @@ from PySide.QtGui import QApplication -def to(text: str): +def to(text: str) -> None: app = QApplication([]) app.clipboard().setText(text) app = None diff --git a/copy2clipboard__via_pyperclip.py b/copy2clipboard__via_pyperclip.py index 389413c51..7db33e437 100644 --- a/copy2clipboard__via_pyperclip.py +++ b/copy2clipboard__via_pyperclip.py @@ -7,7 +7,7 @@ import pyperclip -def to(text: str): +def to(text: str) -> None: pyperclip.copy(text) pyperclip.paste() diff --git a/copy_example.py b/copy_example.py index c5ea1673e..b511a48da 100644 --- a/copy_example.py +++ b/copy_example.py @@ -6,22 +6,40 @@ """RU: Пример использования модуля copy.""" -# TODO: https://docs.python.org/3.4/library/copy.html -# TODO: больше примеров - import copy -if __name__ == "__main__": - a = [2, 3, [3.5, 3.6, [3.61, 3.62]], 4, 5] - print(a, type(a), hex(id(a)), sep=", ") +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=", ") +_print_complex_data(copy.deepcopy(complex_data)) 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/create_vars__use_globals.py b/create_vars__use_globals.py index d994c9077..78c52f113 100644 --- a/create_vars__use_globals.py +++ b/create_vars__use_globals.py @@ -18,7 +18,7 @@ number = 0 -def counter(): +def counter() -> None: # If not exists global # if 'number' not in globals(): # globals()['number'] = 0 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/css_to_xpath__gui/main.py b/css_to_xpath__gui/main.py index ab875202e..7a78a57ce 100644 --- a/css_to_xpath__gui/main.py +++ b/css_to_xpath__gui/main.py @@ -16,7 +16,7 @@ css_to_xpath = HTMLTranslator(xhtml=True).css_to_xpath -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -29,7 +29,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("css_to_xpath__gui") @@ -71,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() @@ -100,7 +100,7 @@ def on_process(self): self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): + def show_detail_error_message(self) -> None: message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = Qt.QErrorMessage() diff --git a/curtain for sleeping.py b/curtain for sleeping.py index 110281b9e..e9744f935 100644 --- a/curtain for sleeping.py +++ b/curtain for sleeping.py @@ -33,7 +33,7 @@ class CurtainWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Curtain for sleeping") @@ -60,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() @@ -73,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() @@ -83,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) diff --git a/custom_with__context_manager/sqlite_execute.py b/custom_with__context_manager/sqlite_execute.py index f07f5a261..c27bb8f52 100644 --- a/custom_with__context_manager/sqlite_execute.py +++ b/custom_with__context_manager/sqlite_execute.py @@ -6,8 +6,11 @@ import sqlite3 +from types import TracebackType +from typing import Optional, Type -def old_old_variant(): + +def old_old_variant() -> None: connect = sqlite3.connect(":memory:") try: @@ -33,7 +36,7 @@ def old_old_variant(): connect.close() -def old_variant(): +def old_variant() -> None: with sqlite3.connect(":memory:") as connect: print(connect.execute("SELECT sqlite_version();").fetchone()) @@ -55,17 +58,17 @@ def old_variant(): class SQLite3Connect(object): - def __init__(self, database): + 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()) 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 42971ea32..000000000 --- a/custom_with__context_manager/time_this_using_with.py +++ /dev/null @@ -1,39 +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( - f"[{self.title}] total time: {time.clock() - self.start_time:.3f} sec" - ) - - -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/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 2bcda88c4..42eef045d 100644 --- a/datetime_example/datetime_example.py +++ b/datetime_example/datetime_example.py @@ -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_strptime__and__setlocale.py b/datetime_strptime__and__setlocale.py index f34d68f7f..1e69345ba 100644 --- a/datetime_strptime__and__setlocale.py +++ b/datetime_strptime__and__setlocale.py @@ -8,7 +8,7 @@ from datetime import datetime -def check(): +def check() -> None: try: print( datetime.strptime( diff --git a/delete_sublist.py b/delete_sublist.py index c2d3a32af..a8bdc3e50 100644 --- a/delete_sublist.py +++ b/delete_sublist.py @@ -25,7 +25,7 @@ def find_sublist(l, m): pass -def delete_sublist(l, m): +def delete_sublist(l, m) -> None: for i, j in find_sublist(l, m): del l[i:j] diff --git a/design_patterns__examples/Abstract Factory/example.py b/design_patterns__examples/Abstract Factory/example.py index 23ab0a200..5566162ff 100644 --- a/design_patterns__examples/Abstract Factory/example.py +++ b/design_patterns__examples/Abstract Factory/example.py @@ -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") diff --git a/design_patterns__examples/Adapter/example__using_composition.py b/design_patterns__examples/Adapter/example__using_composition.py index d876d7176..008874430 100644 --- a/design_patterns__examples/Adapter/example__using_composition.py +++ b/design_patterns__examples/Adapter/example__using_composition.py @@ -18,17 +18,17 @@ def get_picture(self): class GameConsole: - def create_game_picture(self): + def create_game_picture(self) -> str: return "picture from console" class Antenna: - def create_wave_picture(self): + 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,7 +44,7 @@ def get_picture(self): class TV: - def __init__(self, source: SourceAdapter): + def __init__(self, source: SourceAdapter) -> None: self.source = source def show_picture(self): diff --git a/design_patterns__examples/Adapter/example__using_inheritance.py b/design_patterns__examples/Adapter/example__using_inheritance.py index 6caaa7d33..6fff68655 100644 --- a/design_patterns__examples/Adapter/example__using_inheritance.py +++ b/design_patterns__examples/Adapter/example__using_inheritance.py @@ -18,12 +18,12 @@ def get_picture(self): class GameConsole: - def create_game_picture(self): + def create_game_picture(self) -> str: return "picture from console" class Antenna: - def create_wave_picture(self): + def create_wave_picture(self) -> str: return "picture from wave" @@ -38,7 +38,7 @@ def get_picture(self): class TV: - def __init__(self, source: SourceAdapter): + def __init__(self, source: SourceAdapter) -> None: self.source = source def show_picture(self): diff --git a/design_patterns__examples/Bridge/example_1/devices/radio.py b/design_patterns__examples/Bridge/example_1/devices/radio.py index 0dfa9888a..d639c3eba 100644 --- a/design_patterns__examples/Bridge/example_1/devices/radio.py +++ b/design_patterns__examples/Bridge/example_1/devices/radio.py @@ -8,7 +8,7 @@ class Radio(Device): - def __init__(self): + def __init__(self) -> None: self._on = False self._volume = 30 self._channel = 1 @@ -16,16 +16,16 @@ def __init__(self): 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: @@ -36,10 +36,10 @@ def set_volume(self, volume: int): 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 fc884e464..5adfc0437 100644 --- a/design_patterns__examples/Bridge/example_1/devices/tv.py +++ b/design_patterns__examples/Bridge/example_1/devices/tv.py @@ -8,7 +8,7 @@ class Tv(Device): - def __init__(self): + def __init__(self) -> None: self._on = False self._volume = 30 self._channel = 1 @@ -16,16 +16,16 @@ def __init__(self): 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: @@ -36,10 +36,10 @@ def set_volume(self, volume: int): 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 53354a257..745a2a1f5 100644 --- a/design_patterns__examples/Bridge/example_1/main.py +++ b/design_patterns__examples/Bridge/example_1/main.py @@ -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() 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 ef84acb61..c0367b280 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py +++ b/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py @@ -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 8c6bf5385..80b681269 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py +++ b/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py @@ -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_2.py b/design_patterns__examples/Bridge/example_2.py index 223c133e9..3e2248dd7 100644 --- a/design_patterns__examples/Bridge/example_2.py +++ b/design_patterns__examples/Bridge/example_2.py @@ -20,7 +20,7 @@ def draw_circle(self, x: int, y: int, radius: int): class SmallCircleDrawer(Drawer): RADIUS_MULTIPLIER = 0.25 - def draw_circle(self, x: int, y: int, radius: int): + def draw_circle(self, x: int, y: int, radius: int) -> None: print( f"Small circle center = {x},{y} radius = {radius * self.RADIUS_MULTIPLIER}" ) @@ -29,37 +29,37 @@ def draw_circle(self, x: int, y: int, radius: int): class LargeCircleDrawer(Drawer): RADIUS_MULTIPLIER = 10 - def draw_circle(self, x: int, y: int, radius: int): + 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: @@ -71,13 +71,13 @@ 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 diff --git a/design_patterns__examples/Builder/example.py b/design_patterns__examples/Builder/example.py index 549cd2c36..ea97765c8 100644 --- a/design_patterns__examples/Builder/example.py +++ b/design_patterns__examples/Builder/example.py @@ -13,84 +13,84 @@ # "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() diff --git a/design_patterns__examples/Builder/example2.py b/design_patterns__examples/Builder/example2.py index f7b067050..9acb0c03d 100644 --- a/design_patterns__examples/Builder/example2.py +++ b/design_patterns__examples/Builder/example2.py @@ -4,27 +4,30 @@ __author__ = "ipetrash" +from typing import Any, Self + + class Foo: - def __init__(self): + def __init__(self) -> None: self.items = [] self.key_by_value = dict() - def add_item(self, value): + def add_item(self, value) -> Self: self.items.append(value) return self - def add_items(self, values): + def add_items(self, values) -> Self: self.items += values return self - def set_value(self, key, value): + def set_value(self, key, value) -> Self: self.key_by_value[key] = value return self - def get_value(self, key): + def get_value(self, key) -> Any: return self.key_by_value[key] - def __repr__(self): + def __repr__(self) -> str: return f"Foo" diff --git a/design_patterns__examples/Builder/example_wok.py b/design_patterns__examples/Builder/example_wok.py index 4dae818e4..45e91d4f3 100644 --- a/design_patterns__examples/Builder/example_wok.py +++ b/design_patterns__examples/Builder/example_wok.py @@ -149,7 +149,7 @@ 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: @@ -187,7 +187,7 @@ 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": diff --git a/design_patterns__examples/Chain of responsibility/example_1.py b/design_patterns__examples/Chain of responsibility/example_1.py index 45cfa0b43..388756434 100644 --- a/design_patterns__examples/Chain of responsibility/example_1.py +++ b/design_patterns__examples/Chain of responsibility/example_1.py @@ -25,10 +25,10 @@ def set_next(self, handler: "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,10 +51,10 @@ 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}"!' @@ -64,7 +64,7 @@ def handle(self, obj): if __name__ == "__main__": - def client_code(handler: Handler): + def client_code(handler: Handler) -> None: for obj in [None, "123", "456", "111", 456]: print(f"Object {repr(obj)} is ", end="") diff --git a/design_patterns__examples/Chain of responsibility/example_2.py b/design_patterns__examples/Chain of responsibility/example_2.py index 63e6002bc..b1b310fe3 100644 --- a/design_patterns__examples/Chain of responsibility/example_2.py +++ b/design_patterns__examples/Chain of responsibility/example_2.py @@ -10,39 +10,39 @@ 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) diff --git a/design_patterns__examples/Chain of responsibility/example_3.py b/design_patterns__examples/Chain of responsibility/example_3.py index 1ef248c00..7b6461f84 100644 --- a/design_patterns__examples/Chain of responsibility/example_3.py +++ b/design_patterns__examples/Chain of responsibility/example_3.py @@ -38,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 @@ -62,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. @@ -91,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. @@ -109,7 +109,7 @@ class EmailLogger(Logger): msg (str): Message string. """ - def write_message(self, msg: str): + def write_message(self, msg: str) -> None: print("Sending via email:", msg) @@ -121,11 +121,11 @@ class FileLogger(Logger): 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 c02b511ea..4e1f86e83 100644 --- a/design_patterns__examples/Chain of responsibility/example_4.py +++ b/design_patterns__examples/Chain of responsibility/example_4.py @@ -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,7 +24,7 @@ def __init__(self, complexity: int, description: str): # Абстрактный полицейский, который может заниматься расследованием преступлений class Policeman(ABC): - def __init__(self, deduction: int): + def __init__(self, deduction: int) -> None: # Дедукция (умение распутывать сложные дела) у данного полицейского self.deduction = deduction @@ -43,7 +43,7 @@ def set_next(self, policeman: "Policeman") -> "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) @@ -55,19 +55,19 @@ def investigate(self, criminal_action: CriminalAction): class MartinRiggs(Policeman): - def _investigate_сoncrete(self, description: str): + def _investigate_сoncrete(self, description: str) -> None: print('Расследование по делу "' + description + '" ведет сержант Мартин Риггс') class JohnMcClane(Policeman): - def _investigate_сoncrete(self, description: str): + def _investigate_сoncrete(self, description: str) -> None: print( 'Расследование по делу "' + description + '" ведет детектив Джон Макклейн' ) class VincentHanna(Policeman): - def _investigate_сoncrete(self, description: str): + def _investigate_сoncrete(self, description: str) -> None: print( 'Расследование по делу "' + description + '" ведет лейтенант Винсент Ханна' ) diff --git a/design_patterns__examples/Chain of responsibility/example_6.py b/design_patterns__examples/Chain of responsibility/example_6.py index a1544e3d4..0b5be3e07 100644 --- a/design_patterns__examples/Chain of responsibility/example_6.py +++ b/design_patterns__examples/Chain of responsibility/example_6.py @@ -20,7 +20,7 @@ def get_timestamp() -> int: # Базовый класс цепочки. class Middleware(ABC): - def __init__(self): + def __init__(self) -> None: self._next: "Middleware" = None # Помогает строить цепь из объектов-проверок. @@ -44,7 +44,7 @@ def _check_next(self, email: str, password: str) -> bool: # Конкретный элемент цепи обрабатывает запрос по-своему. class ThrottlingMiddleware(Middleware): - def __init__(self, request_per_minute: int): + def __init__(self, request_per_minute: int) -> None: super().__init__() self._request: int = 0 @@ -72,7 +72,7 @@ def check(self, email: str, password: str) -> bool: # Конкретный элемент цепи обрабатывает запрос по-своему. class UserExistsMiddleware(Middleware): - def __init__(self, server: "Server"): + def __init__(self, server: "Server") -> None: super().__init__() self._server: Server = server @@ -102,13 +102,13 @@ def check(self, email: str, password: str) -> bool: # Класс сервера. 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 и пароль от клиента и запускает проверку @@ -123,7 +123,7 @@ def log_in(self, email: str, password: str) -> bool: 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: diff --git a/design_patterns__examples/Command/example.py b/design_patterns__examples/Command/example.py index 866378b56..e21d52dd6 100644 --- a/design_patterns__examples/Command/example.py +++ b/design_patterns__examples/Command/example.py @@ -19,34 +19,34 @@ def execute(self): class Car: - def start_engine(self): + def start_engine(self) -> None: print("Запустить двигатель") - def stop_engine(self): + def stop_engine(self) -> None: print("Остановить двигатель") class StartCar(Command): - def __init__(self, car: Car): + def __init__(self, car: Car) -> None: self.car: Car = car - def execute(self): + def execute(self) -> None: self.car.start_engine() class StopCar(Command): - def __init__(self, car: Car): + def __init__(self, car: Car) -> None: self.car: Car = car - def execute(self): + def execute(self) -> None: self.car.stop_engine() class CarInvoker: - def __init__(self, command: Command): + def __init__(self, command: Command) -> None: self.command: Command = command - def execute(self): + def execute(self) -> None: self.command.execute() diff --git a/design_patterns__examples/Composite/example.py b/design_patterns__examples/Composite/example.py index 53e69a2e5..0c3b6fc2c 100644 --- a/design_patterns__examples/Composite/example.py +++ b/design_patterns__examples/Composite/example.py @@ -19,65 +19,65 @@ 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): + 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): + 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): + 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): + def draw(self, *args, **kwargs) -> None: print(f"Line: x1={self.x1}, y1={self.y1}, x2={self.x2}, y2={self.y2}") diff --git a/design_patterns__examples/Composite/example__pyqt_draw.py b/design_patterns__examples/Composite/example__pyqt_draw.py index 35022ecef..a9e97de75 100644 --- a/design_patterns__examples/Composite/example__pyqt_draw.py +++ b/design_patterns__examples/Composite/example__pyqt_draw.py @@ -17,7 +17,7 @@ from PyQt5.Qt import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -31,82 +31,82 @@ def log_uncaught_exceptions(ex_cls, ex, tb): 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.graphic = graphic - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(Qt.green) diff --git a/design_patterns__examples/Decorator/example_1.py b/design_patterns__examples/Decorator/example_1.py index 8fd09366a..12edf7a6f 100644 --- a/design_patterns__examples/Decorator/example_1.py +++ b/design_patterns__examples/Decorator/example_1.py @@ -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 diff --git a/design_patterns__examples/Decorator/example_2.py b/design_patterns__examples/Decorator/example_2.py index ff6c8f878..87a965c17 100644 --- a/design_patterns__examples/Decorator/example_2.py +++ b/design_patterns__examples/Decorator/example_2.py @@ -10,44 +10,44 @@ 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!") diff --git a/design_patterns__examples/Facade/example_1.py b/design_patterns__examples/Facade/example_1.py index 4a120dc39..9142444d4 100644 --- a/design_patterns__examples/Facade/example_1.py +++ b/design_patterns__examples/Facade/example_1.py @@ -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,13 +46,13 @@ 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, diff --git a/design_patterns__examples/Facade/example_3.py b/design_patterns__examples/Facade/example_3.py index a4826d882..468c49830 100644 --- a/design_patterns__examples/Facade/example_3.py +++ b/design_patterns__examples/Facade/example_3.py @@ -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() diff --git a/design_patterns__examples/Factory/example_2.py b/design_patterns__examples/Factory/example_2.py index b6551353a..ae56828b1 100644 --- a/design_patterns__examples/Factory/example_2.py +++ b/design_patterns__examples/Factory/example_2.py @@ -21,17 +21,17 @@ def initial(animal: str) -> "Animal": 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") diff --git a/design_patterns__examples/Flyweight/example_1.py b/design_patterns__examples/Flyweight/example_1.py index 6384760aa..5f91166f1 100644 --- a/design_patterns__examples/Flyweight/example_1.py +++ b/design_patterns__examples/Flyweight/example_1.py @@ -12,16 +12,16 @@ 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): + def report(self, col: int) -> None: print(f" {self.row}{col}", end="") class Factory: - def __init__(self, max_rows: int): + def __init__(self, max_rows: int) -> None: self._pool: list[Flyweight | None] = [None] * max_rows def get_flyweight(self, row: int) -> Flyweight: diff --git a/design_patterns__examples/Flyweight/example_2.py b/design_patterns__examples/Flyweight/example_2.py index 809e30af4..f945f6de2 100644 --- a/design_patterns__examples/Flyweight/example_2.py +++ b/design_patterns__examples/Flyweight/example_2.py @@ -20,14 +20,14 @@ 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(f"{self.symbol} (point_size {self.point_size})") # "FlyweightFactory" class CharacterFactory: - def __init__(self): + def __init__(self) -> None: self._characters: dict[str, Character] = dict() def get_character(self, key: str) -> Character: @@ -52,7 +52,7 @@ def get_character(self, key: str) -> Character: # "ConcreteFlyweight" class CharacterA(Character): - def __init__(self): + def __init__(self) -> None: self.symbol = "A" self.height = 100 self.width = 120 @@ -62,7 +62,7 @@ def __init__(self): # "ConcreteFlyweight" class CharacterB(Character): - def __init__(self): + def __init__(self) -> None: self.symbol = "B" self.height = 100 self.width = 140 @@ -75,7 +75,7 @@ def __init__(self): # "ConcreteFlyweight" class CharacterZ(Character): - def __init__(self): + def __init__(self) -> None: self.symbol = "Z" self.height = 100 self.width = 100 diff --git a/design_patterns__examples/Mediator/example_1.py b/design_patterns__examples/Mediator/example_1.py index 5ad6c2649..2309230b6 100644 --- a/design_patterns__examples/Mediator/example_1.py +++ b/design_patterns__examples/Mediator/example_1.py @@ -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": @@ -57,31 +57,31 @@ 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") diff --git a/design_patterns__examples/Mediator/example_2.py b/design_patterns__examples/Mediator/example_2.py index 81ec8852c..f0ccd146b 100644 --- a/design_patterns__examples/Mediator/example_2.py +++ b/design_patterns__examples/Mediator/example_2.py @@ -11,15 +11,15 @@ class Mediator: @staticmethod - def send_message(user: "User", msg: str): + 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) diff --git a/design_patterns__examples/Mediator/example_notes__pyqt.py b/design_patterns__examples/Mediator/example_notes__pyqt.py index 364fcec13..e0facea05 100644 --- a/design_patterns__examples/Mediator/example_notes__pyqt.py +++ b/design_patterns__examples/Mediator/example_notes__pyqt.py @@ -30,7 +30,7 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -44,14 +44,14 @@ def log_uncaught_exceptions(ex_cls, ex, tb): # Класс заметок class Note: - def __init__(self): + def __init__(self) -> None: self._name = "note" 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: @@ -62,12 +62,12 @@ def getText(self) -> str: class DefaultListModel(QAbstractListModel): - def __init__(self): + def __init__(self) -> None: super().__init__() 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: @@ -84,7 +84,7 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: 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) @@ -94,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() @@ -109,57 +109,57 @@ def get_items(self) -> list[Note]: # Общий интерфейс посредников. 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 @@ -170,7 +170,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class AddButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Add") # При клике на кнопку вызываем метод посредника @@ -183,7 +183,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class DeleteButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Del") # При клике на кнопку вызываем метод посредника @@ -196,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 @@ -235,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) @@ -250,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 @@ -273,7 +273,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class SaveButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Save") # При клике на кнопку вызываем метод посредника @@ -286,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() @@ -298,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() @@ -311,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 @@ -327,7 +327,7 @@ def __init__(self): self._mainWindow = None # Здесь происходит регистрация компонентов посредником. - def registerComponent(self, component: Component): + def registerComponent(self, component: Component) -> None: component.setMediator(self) if component.getName() == "AddButton": @@ -342,7 +342,7 @@ def registerComponent(self, component: Component): elif component.getName() == "List": self._list: ListNote = component - def foo(*args): + def foo(*args) -> None: empty = len(self._list.selectedIndexes()) == 0 self.hideElements(empty) @@ -366,7 +366,7 @@ def foo(*args): # Разнообразные методы общения с компонентами. # - def addNewNote(self, note: Note): + def addNewNote(self, note: Note) -> None: # Инкрементальные названия text = note.getName() + str(self._list.model().rowCount() + 1) note.setName(text) @@ -375,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): + 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()) @@ -393,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() @@ -408,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) @@ -427,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:")) diff --git a/design_patterns__examples/Observer/example_1.py b/design_patterns__examples/Observer/example_1.py index 678e01cf0..2585c4c06 100644 --- a/design_patterns__examples/Observer/example_1.py +++ b/design_patterns__examples/Observer/example_1.py @@ -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,14 +74,14 @@ 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): + def display(self) -> None: print( f"Сейчас значения: {self.temperature:.1f} градусов цельсия и {self.humidity:.1f}% влажности. " f"Давление {self.pressure} мм рт. ст." diff --git a/design_patterns__examples/Observer/example_4.py b/design_patterns__examples/Observer/example_4.py index 95453aed2..c797b1f59 100644 --- a/design_patterns__examples/Observer/example_4.py +++ b/design_patterns__examples/Observer/example_4.py @@ -31,33 +31,33 @@ def update(self, event_type: str, file: IO): class EventManager: - def __init__(self, *operations): + 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): + def open_file(self, file_path: str) -> None: self.file = open(file_path, "w", encoding="utf-8") self.events.notify("open", self.file) @@ -69,10 +69,10 @@ 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): + 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}" @@ -80,11 +80,11 @@ def update(self, event_type: str, file: IO): 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( diff --git a/design_patterns__examples/Prototype/example.py b/design_patterns__examples/Prototype/example.py index 86aded0b7..d9793cacf 100644 --- a/design_patterns__examples/Prototype/example.py +++ b/design_patterns__examples/Prototype/example.py @@ -9,21 +9,22 @@ 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) @@ -33,13 +34,13 @@ def clone(self, name, **attr): 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): + def __str__(self) -> str: return f"A({self.x}, {self.y}, {self.z}, {self.garbage})" a = A() diff --git a/design_patterns__examples/Proxy/example.py b/design_patterns__examples/Proxy/example.py index d67ba0e66..2c520c2b7 100644 --- a/design_patterns__examples/Proxy/example.py +++ b/design_patterns__examples/Proxy/example.py @@ -43,7 +43,7 @@ def div(self, x, y): class MathProxy(IMath): """Прокси""" - def __init__(self): + def __init__(self) -> None: self.math = None # Быстрые операции - не требуют реального субъекта diff --git a/design_patterns__examples/Proxy/example__cached.py b/design_patterns__examples/Proxy/example__cached.py index f0e37f308..e3c7ee6d7 100644 --- a/design_patterns__examples/Proxy/example__cached.py +++ b/design_patterns__examples/Proxy/example__cached.py @@ -5,6 +5,9 @@ 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() @@ -58,15 +61,20 @@ def get_status_code(self, url: str) -> int: if __name__ == "__main__": - import time + 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(f"Elapsed time: {time.clock() - self.start_time:.6f} sec") + 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" diff --git a/design_patterns__examples/Proxy/example__logged.py b/design_patterns__examples/Proxy/example__logged.py index d45474417..10e59266e 100644 --- a/design_patterns__examples/Proxy/example__logged.py +++ b/design_patterns__examples/Proxy/example__logged.py @@ -8,6 +8,8 @@ import sys from logging.handlers import RotatingFileHandler +from types import TracebackType +from typing import Optional, Type from example__cached import IGoUrl, GoUrl, GoUrlCachedProxy, requests @@ -23,7 +25,9 @@ def get_logger(name, file="log.txt", encoding="utf-8"): # Simple file handler # fh = logging.FileHandler(file, encoding=encoding) # or: - 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) @@ -39,7 +43,7 @@ class GoUrlLoggedProxy(IGoUrl): _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() @@ -61,20 +65,25 @@ 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 + 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(f"Elapsed time: {time.clock() - self.start_time:.6f} sec") + 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" diff --git a/design_patterns__examples/Proxy/example__with_abc.py b/design_patterns__examples/Proxy/example__with_abc.py index 82c9c4f68..30ff7f6b4 100644 --- a/design_patterns__examples/Proxy/example__with_abc.py +++ b/design_patterns__examples/Proxy/example__with_abc.py @@ -49,7 +49,7 @@ def div(self, x, y): class MathProxy(IMath): """Прокси""" - def __init__(self): + def __init__(self) -> None: self.math = None # Быстрые операции - не требуют реального субъекта diff --git a/design_patterns__examples/Strategy/example.py b/design_patterns__examples/Strategy/example.py index 70aa3e7d2..dddbba159 100644 --- a/design_patterns__examples/Strategy/example.py +++ b/design_patterns__examples/Strategy/example.py @@ -19,23 +19,23 @@ 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) diff --git a/design_patterns__examples/Strategy/example_1.py b/design_patterns__examples/Strategy/example_1.py index 11ed51b47..5519920bb 100644 --- a/design_patterns__examples/Strategy/example_1.py +++ b/design_patterns__examples/Strategy/example_1.py @@ -28,10 +28,10 @@ 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): diff --git a/design_patterns__examples/Template method/example_2.py b/design_patterns__examples/Template method/example_2.py index 3a47b5056..6c6b81956 100644 --- a/design_patterns__examples/Template method/example_2.py +++ b/design_patterns__examples/Template method/example_2.py @@ -18,19 +18,19 @@ 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() @@ -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 ... diff --git a/design_patterns__examples/Template method/example_3.py b/design_patterns__examples/Template method/example_3.py index 234fd0b7b..6ea680ff2 100644 --- a/design_patterns__examples/Template method/example_3.py +++ b/design_patterns__examples/Template method/example_3.py @@ -41,13 +41,13 @@ 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 @@ -68,10 +68,10 @@ 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: @@ -87,7 +87,7 @@ def _simulate_network_latency(self): # Класс социальной сети. 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 @@ -108,10 +108,10 @@ 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: diff --git a/design_patterns__examples/Template method/example_4.py b/design_patterns__examples/Template method/example_4.py index 076c6c080..31c765e05 100644 --- a/design_patterns__examples/Template method/example_4.py +++ b/design_patterns__examples/Template method/example_4.py @@ -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,14 +27,14 @@ 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() @@ -44,11 +44,11 @@ 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: @@ -60,11 +60,11 @@ 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 @@ -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,24 +110,24 @@ 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: # Отправить воинов на позицию. ... diff --git a/detection_of_site_changes_Unistream/gui.py b/detection_of_site_changes_Unistream/gui.py index 703ba6118..afaf178f9 100644 --- a/detection_of_site_changes_Unistream/gui.py +++ b/detection_of_site_changes_Unistream/gui.py @@ -22,7 +22,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("detection_of_site_changes_Unistream") @@ -53,7 +53,7 @@ def __init__(self): 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" diff --git a/detection_of_site_changes_Unistream/main.py b/detection_of_site_changes_Unistream/main.py index 2267877cd..bfb3676bb 100644 --- a/detection_of_site_changes_Unistream/main.py +++ b/detection_of_site_changes_Unistream/main.py @@ -57,7 +57,7 @@ def get_site_text(url="https://test.api.unistream.com/help/index.html"): ) class WebPage(QWebPage): - def userAgentForUrl(self, url): + 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: @@ -149,7 +149,7 @@ class TextRevision(Base): # Содержимым является только разница diff = Column(String) - def __init__(self, new_text, old_text=""): + def __init__(self, new_text, old_text="") -> None: """ Конструктор принимает контент и сравниваемый контент, запоминает хеш содержимого, текущую дату и время и результат сравнения @@ -166,7 +166,7 @@ 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): + def __repr__(self) -> str: return f"" diff --git a/dict_to_url_params.py b/dict_to_url_params.py index 45e869033..c30173bf5 100644 --- a/dict_to_url_params.py +++ b/dict_to_url_params.py @@ -5,7 +5,7 @@ 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}]" diff --git a/dis__bytecode__is_empty_function.py b/dis__bytecode__is_empty_function.py index 2d7e1768e..7f54f9bea 100644 --- a/dis__bytecode__is_empty_function.py +++ b/dis__bytecode__is_empty_function.py @@ -20,10 +20,10 @@ def check_is_empty_function(function): if __name__ == "__main__": - def foo(): + def foo() -> None: pass - def foo2(): + def foo2() -> int: return 1 print("Empty:", check_is_empty_function(foo)) diff --git a/download_file/download_file.py b/download_file/download_file.py index 81641a7fa..bb3e5e297 100644 --- a/download_file/download_file.py +++ b/download_file/download_file.py @@ -28,26 +28,26 @@ def wrapper(*args, **kwargs): @timer -def way1(url: str, file_name: str): +def way1(url: str, file_name: str) -> None: resource = urlopen(url) with open(file_name, "wb") as f: f.write(resource.read()) @timer -def way2(url: str, file_name: str): +def way2(url: str, file_name: str) -> None: urlretrieve(url, file_name) @timer -def way3(url: str, file_name: str): +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: str, file_name: str): +def way4(url: str, file_name: str) -> None: h = httplib2.Http(".cache") response, content = h.request(url) with open(file_name, "wb") as f: @@ -55,7 +55,7 @@ def way4(url: str, file_name: str): @timer -def way5(url: str, file_name: str): +def way5(url: str, file_name: str) -> None: g = Grab() g.go(url) g.response.save(file_name) diff --git a/download_file/with_progress.py b/download_file/with_progress.py index 14288e6e2..4eccbcc5a 100644 --- a/download_file/with_progress.py +++ b/download_file/with_progress.py @@ -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 @@ -56,7 +56,7 @@ def run(url, file_name, reporthook, callback_func): print(download(URL, "SimplePyScripts.zip", as_thread=True)) - def callback_func(file_name: str): + def callback_func(file_name: str) -> None: print("File name:", file_name) print( diff --git a/download_volume_readmanga.py b/download_volume_readmanga.py index 8f9143f81..b29ce0054 100644 --- a/download_volume_readmanga.py +++ b/download_volume_readmanga.py @@ -39,7 +39,7 @@ def get_url_images(url): 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писок изображений пустой.") return diff --git a/draw fractal/Apollon_Set/Apollon_Set__PIL.py b/draw fractal/Apollon_Set/Apollon_Set__PIL.py index 6e54f6655..0359ebadc 100644 --- a/draw fractal/Apollon_Set/Apollon_Set__PIL.py +++ b/draw fractal/Apollon_Set/Apollon_Set__PIL.py @@ -69,7 +69,7 @@ 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 diff --git a/draw fractal/Cantor_dust/Cantor_dust__PIL.py b/draw fractal/Cantor_dust/Cantor_dust__PIL.py index 4b6e59555..3dd63d6aa 100644 --- a/draw fractal/Cantor_dust/Cantor_dust__PIL.py +++ b/draw fractal/Cantor_dust/Cantor_dust__PIL.py @@ -40,8 +40,8 @@ from PIL import Image, ImageDraw -def draw_cantor_dust(draw_by_image): - def draw(x, y, size): +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) diff --git a/draw fractal/Circular_fractal/Circular_fractal__PIL.py b/draw fractal/Circular_fractal/Circular_fractal__PIL.py index f74d1c0b9..28735840b 100644 --- a/draw fractal/Circular_fractal/Circular_fractal__PIL.py +++ b/draw fractal/Circular_fractal/Circular_fractal__PIL.py @@ -39,7 +39,7 @@ 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 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 9cd17d4fd..dad5d9eb2 100644 --- a/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py +++ b/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py @@ -40,7 +40,7 @@ from PIL import Image, ImageDraw -def draw_dragon_curve_1(draw_by_image, x1, y1, x2, y2, k): +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 diff --git a/draw fractal/Fern/Fern__PIL.py b/draw fractal/Fern/Fern__PIL.py index e19823408..5353cc35b 100644 --- a/draw fractal/Fern/Fern__PIL.py +++ b/draw fractal/Fern/Fern__PIL.py @@ -49,7 +49,7 @@ from PIL import Image, ImageDraw -def draw_fern(draw_by_image, width, height): +def draw_fern(draw_by_image, width, height) -> None: n = 255 cx = 0.251 diff --git a/draw fractal/Fern_2/Fern_2__PIL.py b/draw fractal/Fern_2/Fern_2__PIL.py index 0f850aabb..9662a6311 100644 --- a/draw fractal/Fern_2/Fern_2__PIL.py +++ b/draw fractal/Fern_2/Fern_2__PIL.py @@ -46,11 +46,11 @@ from PIL import Image, ImageDraw -def draw_fern_2(draw_by_image): - def line_to(x, y, l, u): +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) diff --git a/draw fractal/Fingerprint/Fingerprint__PIL.py b/draw fractal/Fingerprint/Fingerprint__PIL.py index 676cdbff6..ad6415f4f 100644 --- a/draw fractal/Fingerprint/Fingerprint__PIL.py +++ b/draw fractal/Fingerprint/Fingerprint__PIL.py @@ -48,7 +48,7 @@ from PIL import Image, ImageDraw -def draw_fingerprint(draw_by_image, width, height): +def draw_fingerprint(draw_by_image, width, height) -> None: n = 255 max = 10 diff --git a/draw fractal/Fractal_tree/Fractal_tree__PIL.py b/draw fractal/Fractal_tree/Fractal_tree__PIL.py index baa0f658d..97316863e 100644 --- a/draw fractal/Fractal_tree/Fractal_tree__PIL.py +++ b/draw fractal/Fractal_tree/Fractal_tree__PIL.py @@ -64,7 +64,7 @@ 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 diff --git a/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py b/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py index 0389a994c..7495fbfde 100644 --- a/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py +++ b/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py @@ -59,7 +59,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Fractal tree") @@ -76,7 +76,7 @@ def __init__(self): self.setLayout(main_layout) - def generate_tree(self): + def generate_tree(self) -> None: img = Image.new("RGB", (700, 600), "white") draw_fractal_tree(ImageDraw.Draw(img), 350, 580, 3 * math.pi / 2, 200) diff --git a/draw fractal/Gosper_curve/Gosper_curve__PIL.py b/draw fractal/Gosper_curve/Gosper_curve__PIL.py index 7add7cecf..dd23d5b81 100644 --- a/draw fractal/Gosper_curve/Gosper_curve__PIL.py +++ b/draw fractal/Gosper_curve/Gosper_curve__PIL.py @@ -61,8 +61,8 @@ 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) 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 9ecb1c28d..a4628ee1c 100644 --- a/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py +++ b/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py @@ -49,12 +49,12 @@ 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) 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 a447e22b1..38e5d0d56 100644 --- a/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py +++ b/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py @@ -51,12 +51,12 @@ 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) diff --git a/draw fractal/Koch_curve/Koch_curve__PIL.py b/draw fractal/Koch_curve/Koch_curve__PIL.py index 17b9e2dd7..db728139f 100644 --- a/draw fractal/Koch_curve/Koch_curve__PIL.py +++ b/draw fractal/Koch_curve/Koch_curve__PIL.py @@ -60,7 +60,7 @@ from PIL import Image, ImageDraw -def draw_koch(draw, xa, ya, xe, ye, n): +def draw_koch(draw, xa, ya, xe, ye, n) -> None: """ Draws koch curve between two points. diff --git a/draw fractal/Koch_curve/Koch_curve__Qt.py b/draw fractal/Koch_curve/Koch_curve__Qt.py index 67a5346cb..90aa52c09 100644 --- a/draw fractal/Koch_curve/Koch_curve__Qt.py +++ b/draw fractal/Koch_curve/Koch_curve__Qt.py @@ -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. diff --git a/draw fractal/Koch_curve/Koch_curve__Qt_gui.py b/draw fractal/Koch_curve/Koch_curve__Qt_gui.py index 1dad3cf96..0996c3e50 100644 --- a/draw fractal/Koch_curve/Koch_curve__Qt_gui.py +++ b/draw fractal/Koch_curve/Koch_curve__Qt_gui.py @@ -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. @@ -130,7 +130,7 @@ def draw_koch(painter, xa, ya, xe, ye, n): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Koch_curve snowflake") @@ -155,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) diff --git a/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py b/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py index 3d3fb0f43..8a97d9a14 100644 --- a/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py +++ b/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py @@ -50,13 +50,13 @@ 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" diff --git a/draw fractal/Levy_curve/Levy_curve__PIL.py b/draw fractal/Levy_curve/Levy_curve__PIL.py index 369776b57..50cb3752d 100644 --- a/draw fractal/Levy_curve/Levy_curve__PIL.py +++ b/draw fractal/Levy_curve/Levy_curve__PIL.py @@ -56,7 +56,7 @@ from PIL import Image, ImageDraw -def draw_levy(draw): +def draw_levy(draw) -> None: iter = 50000 mx = 200 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 0048dc005..5c6a3ed5b 100644 --- a/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py +++ b/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py @@ -48,7 +48,7 @@ from PIL import Image, ImageDraw -def draw_mandelbrot_set_1(draw_by_image, width, height): +def draw_mandelbrot_set_1(draw_by_image, width, height) -> None: n = 255 max = 10 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 9467f811e..d03467eb2 100644 --- a/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py +++ b/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py @@ -48,7 +48,7 @@ from PIL import Image, ImageDraw -def draw_mandelbrot_set_2(draw_by_image, width, height): +def draw_mandelbrot_set_2(draw_by_image, width, height) -> None: n = 255 max = 10 diff --git a/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py b/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py index e282614e6..f8f631a4e 100644 --- a/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py +++ b/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py @@ -79,7 +79,7 @@ from PIL import Image, ImageDraw -def draw_minkowski(draw, xa, ya, xi, yi, n): +def draw_minkowski(draw, xa, ya, xi, yi, n) -> None: """ Draws minkowski curve between two points. diff --git a/draw fractal/Monkey_tree/Monkey_tree__PIL.py b/draw fractal/Monkey_tree/Monkey_tree__PIL.py index 74cd0ff9a..e1d71f847 100644 --- a/draw fractal/Monkey_tree/Monkey_tree__PIL.py +++ b/draw fractal/Monkey_tree/Monkey_tree__PIL.py @@ -77,12 +77,12 @@ from PIL import Image, ImageDraw -def draw_monkey_tree(draw_by_image): +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): + def draw(x, y, l, u, t, q, s) -> None: if t > 0: if q == 1: x += l * cos(u) 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 fd56eb7fa..05c44e2a6 100644 --- a/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py +++ b/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py @@ -45,11 +45,11 @@ from PIL import Image, ImageDraw -def draw_pythagoras_tree_2(draw_by_image): - def line_to(x, y, l, u): +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) diff --git a/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py b/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py index 4b249abbe..4ecafccf6 100644 --- a/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py +++ b/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py @@ -50,8 +50,8 @@ from PIL import Image, ImageDraw -def draw_sierpinski_carpet(draw_by_image, Z): - def serp(x1, y1, x2, y2, n): +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 diff --git a/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py b/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py index 6add5bead..7037a8d6e 100644 --- a/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py +++ b/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py @@ -52,15 +52,15 @@ from PIL import Image, ImageDraw -def draw_sierpinski_triangle(draw_by_image, Z): +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 diff --git a/draw fractal/Snowflake/Snowflake__PIL.py b/draw fractal/Snowflake/Snowflake__PIL.py index ce9dba293..28ca5b02c 100644 --- a/draw fractal/Snowflake/Snowflake__PIL.py +++ b/draw fractal/Snowflake/Snowflake__PIL.py @@ -39,8 +39,8 @@ 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): diff --git a/draw_wave.py b/draw_wave.py index b9beb005a..a84976c18 100644 --- a/draw_wave.py +++ b/draw_wave.py @@ -44,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() diff --git a/dynamic_methods_link_call.py b/dynamic_methods_link_call.py index 93793c5a6..7755ae318 100644 --- a/dynamic_methods_link_call.py +++ b/dynamic_methods_link_call.py @@ -5,7 +5,7 @@ class CallBuilder: - def __init__(self, part=None, sep=""): + def __init__(self, part=None, sep="") -> None: self._part = part self._sep = sep diff --git a/effect_of_vanishing_photos/effect_of_vanishing_photos.py b/effect_of_vanishing_photos/effect_of_vanishing_photos.py index 7153581b3..37ec67af4 100644 --- a/effect_of_vanishing_photos/effect_of_vanishing_photos.py +++ b/effect_of_vanishing_photos/effect_of_vanishing_photos.py @@ -31,7 +31,7 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -45,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() @@ -68,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() @@ -80,7 +80,7 @@ def tick(self): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("effect_of_vanishing_photos.py") @@ -91,12 +91,12 @@ def __init__(self): 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) diff --git a/enum__examples.py b/enum__examples.py index 3b7bc718e..2c9cbae63 100644 --- a/enum__examples.py +++ b/enum__examples.py @@ -60,7 +60,7 @@ class Planet(Enum): URANUS = (8.686e25, 2.5559e7) NEPTUNE = (1.024e26, 2.4746e7) - def __init__(self, mass, radius): + def __init__(self, mass, radius) -> None: self.mass = mass # in kilograms self.radius = radius # in meters diff --git a/eval_expr_with_time.py b/eval_expr_total_time.py similarity index 50% rename from eval_expr_with_time.py rename to eval_expr_total_time.py index f3d466958..10fa6bdc0 100644 --- a/eval_expr_with_time.py +++ b/eval_expr_total_time.py @@ -8,10 +8,12 @@ from seconds_to_str import seconds_to_str -PATTERN_TIME = re.compile(r"(\d\d:\d\d:\d\d)") -PATTERN_EXPR_WITH_TIME = re.compile( - f"^{PATTERN_TIME.pattern}(?: [+-] {PATTERN_TIME.pattern})*$" +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: @@ -23,20 +25,44 @@ 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 = preprocess_expr_with_time(text) - total_seconds = eval(text) + 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" - - print(eval_expr_with_time(text)) - # 10:34:23 + 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 diff --git a/exit_handler.py b/exit_handler.py index dbd843a3d..b4a31d9a0 100644 --- a/exit_handler.py +++ b/exit_handler.py @@ -11,7 +11,7 @@ start_time = timer() -def exit_handler(): +def exit_handler() -> None: print(f"Execution time: {timer() - start_time:.3f} secs.") @@ -20,7 +20,7 @@ def exit_handler(): # OR with decorator: @atexit.register -def exit_handler(): +def exit_handler() -> None: print(f"Execution time: {timer() - start_time:.3f} secs.") diff --git a/explore__windows.py b/explore__windows.py index 6dc61ba18..818614c91 100644 --- a/explore__windows.py +++ b/explore__windows.py @@ -9,7 +9,7 @@ from pathlib import Path -def explore(path: str | Path, select=True): +def explore(path: str | Path, select=True) -> None: path = Path(path).resolve() if path.is_dir() or path.is_file(): 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 ef665738e..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 @@ -53,7 +53,7 @@ # Formatted string literals cannot be used as docstrings, even if they do not include expressions. -def foo(): +def foo() -> None: f"Not a docstring" 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 05a16f2cd..b8e6168e4 100644 --- a/f-strings__formatted string literals__PEP 498/my_example.py +++ b/f-strings__formatted string literals__PEP 498/my_example.py @@ -41,17 +41,17 @@ def strange_1(text): # Use class class Foo: - def __init__(self, text=""): + def __init__(self, text="") -> None: self.text = text def strange_2(self, text): return re.sub(r"[aeo]", " ", text) @staticmethod - def strange_3(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: @@ -65,10 +65,10 @@ def __format__(self, format_spec): return self.__str__() - def __str__(self): + def __str__(self) -> str: return f'' - def __repr__(self): + def __repr__(self) -> str: return f'' 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/games/tetris/config.py b/fastapi__examples/blog_from_stepic/src/blog/__init__.py similarity index 81% rename from games/tetris/config.py rename to fastapi__examples/blog_from_stepic/src/blog/__init__.py index b403d7edb..f25732790 100644 --- a/games/tetris/config.py +++ b/fastapi__examples/blog_from_stepic/src/blog/__init__.py @@ -2,6 +2,3 @@ # -*- coding: utf-8 -*- __author__ = "ipetrash" - - -DEBUG = False 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/extract_pictures_from_fb2/fb2_pictures__using_bs4.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_bs4.py index 19a1ea629..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 @@ -23,7 +23,7 @@ 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) 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 022520907..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 @@ -23,7 +23,7 @@ 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) @@ -34,7 +34,7 @@ def do(file_name, output_dir="output", debug=True): 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"] 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 0afbe7af7..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 @@ -25,7 +25,7 @@ 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) 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 index dc5110be0..cbe234673 100644 --- 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 @@ -54,7 +54,7 @@ def iter_blocks(text: str, start_str: str, end_str: str) -> Iterator[str]: yield block -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) 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 fc67172be..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 @@ -13,16 +13,17 @@ 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 xml.etree import ElementTree as ET 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) 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 6be8b363c..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 @@ -22,7 +22,7 @@ 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) @@ -36,18 +36,18 @@ def do(file_name, output_dir="output", debug=True): "number": 1, } - def on_start_element(name, attrs): + 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): + def on_char_data(data) -> None: if PARSE_DATA["last_start_tag"] != "binary": return PARSE_DATA["last_tag_data"] += data - def on_end_element(name): + def on_end_element(name) -> None: if name != "binary": return 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 d96312892..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 @@ -22,7 +22,7 @@ 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) @@ -39,18 +39,18 @@ def do(file_name, output_dir="output", debug=True): } class BinaryHandler(xml.sax.ContentHandler): - def startElement(self, name, attrs): + 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): + def characters(self, content) -> None: if PARSE_DATA["last_start_tag"] != "binary": return PARSE_DATA["last_tag_data"] += content - def endElement(self, name): + def endElement(self, name) -> None: if name != "binary": return diff --git a/fb2__parsing/extract_pictures_from_fb2/time_test.py b/fb2__parsing/extract_pictures_from_fb2/time_test.py index 4c9e3cb36..8f1040d7b 100644 --- a/fb2__parsing/extract_pictures_from_fb2/time_test.py +++ b/fb2__parsing/extract_pictures_from_fb2/time_test.py @@ -7,22 +7,26 @@ 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 +count = 20 runs = [ - ("LXML", "do_lxml"), - ("XML EXPAT", "do_xml_expat"), - ("XML.ETREE", "do_xml_etree"), - ("XML SAX", "do_xml_sax"), + # ("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"), diff --git a/fb2__parsing/get_sections.py b/fb2__parsing/get_sections.py index ec6be26b7..fdf63a009 100644 --- a/fb2__parsing/get_sections.py +++ b/fb2__parsing/get_sections.py @@ -9,7 +9,7 @@ def get_sections_as_dict(root) -> dict[str, dict]: # Рекурсивная функция поиска
- def _find_sections(root, root_dict: dict): + def _find_sections(root, root_dict: dict) -> None: for section in root.find_all("section", recursive=False): title = section.title.text.strip() children = dict() @@ -29,7 +29,7 @@ def _find_sections(root, root_dict: dict): def get_sections_as_list(root) -> list[tuple[str, list]]: # Рекурсивная функция поиска
- def _find_sections(root, children_list: list): + def _find_sections(root, children_list: list) -> None: for section in root.find_all("section", recursive=False): title = section.title.text.strip() children = [] @@ -51,7 +51,7 @@ def _find_sections(root, children_list: list): 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", ". ")) if children: diff --git a/fb2__parsing/print_section_by_text_number_length.py b/fb2__parsing/print_section_by_text_number_length.py index 1851f3446..54a691d8e 100644 --- a/fb2__parsing/print_section_by_text_number_length.py +++ b/fb2__parsing/print_section_by_text_number_length.py @@ -9,7 +9,7 @@ def get_sections_as_dict(root) -> tuple[dict[str, dict], dict[str, Tag]]: # Рекурсивная функция поиска
- def _find_sections(root, root_dict: dict, title_by_section: dict): + 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 = dict() @@ -33,7 +33,7 @@ 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): + 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) @@ -64,8 +64,8 @@ def _print_sections( section_by_text: dict, number_length_book_text, level=1 - ): - def _find_section_lines(root, level, lines: list): + ) -> None: + def _find_section_lines(root, level, lines: list) -> None: for title, children in root.items(): text = "{}{}".format(" " * (level - 1), title.replace("\n", ". ")) diff --git a/file/append.py b/file/append.py index e17641cc1..76c047a82 100644 --- a/file/append.py +++ b/file/append.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + __author__ = "ipetrash" @@ -9,12 +12,12 @@ # Открыть файл в режиме добавления записей -with open("foo.txt", mode="a") as f: +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: +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 47a3adc4b..000000000 --- a/file/foo.py +++ /dev/null @@ -1,6 +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 18d28bc8f..61ac67c1d 100644 --- a/file/read.py +++ b/file/read.py @@ -1,16 +1,20 @@ +#!/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 # Открыть файл в режиме чтения -with open("foo.txt", mode="r") as f: +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="") +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 e75e1a87e..c74bcbb19 100644 --- a/file/seek.py +++ b/file/seek.py @@ -4,7 +4,7 @@ __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(f"{i}.") diff --git a/file/write.py b/file/write.py index c5f25069d..5c4fb7898 100644 --- a/file/write.py +++ b/file/write.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + __author__ = "ipetrash" @@ -6,7 +9,7 @@ # Открыть файл в режиме записи -with open("foo.txt", mode="w") as f: +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") diff --git a/file_tree_maker.py b/file_tree_maker.py index eb6561044..67fbab1cd 100644 --- a/file_tree_maker.py +++ b/file_tree_maker.py @@ -13,7 +13,7 @@ class FileTreeMaker: - def _recurse(self, parent_path, file_list, prefix, output_buf, level): + 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: diff --git a/firefox/api.py b/firefox/api.py index 2cd9f4deb..5e509a032 100644 --- a/firefox/api.py +++ b/firefox/api.py @@ -38,7 +38,7 @@ def close_tabs( file_name_session: Path, urls: list[str], log: logging.Logger, -): +) -> None: modified = False json_data = get_sessionstore(file_name_session) @@ -61,7 +61,7 @@ def close_tabs( def close_duplicate_tabs( file_name_session: Path, log: logging.Logger, -): +) -> None: json_data = get_sessionstore(file_name_session) urls = get_tab_urls_from_sessionstore(json_data) @@ -99,7 +99,7 @@ def close_bookmarks( 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 = ?)" diff --git a/firefox/common.py b/firefox/common.py index 114a9f80c..84f3b8578 100644 --- a/firefox/common.py +++ b/firefox/common.py @@ -6,7 +6,7 @@ import logging import sys - +from logging.handlers import RotatingFileHandler from pathlib import Path @@ -21,23 +21,23 @@ def get_logger(file_name: str, dir_name=DIR_LOGS): 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" ) - 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/jsonlz4_mozLz4/mozlz4a.py b/firefox/jsonlz4_mozLz4/mozlz4a.py index 128014ee3..bbee46ea8 100644 --- a/firefox/jsonlz4_mozLz4/mozlz4a.py +++ b/firefox/jsonlz4_mozLz4/mozlz4a.py @@ -79,7 +79,7 @@ def loads_json(file_obj: BinaryIO) -> dict: return json.loads(data) -def dumps_json(file_obj: BinaryIO, json_data: dict): +def dumps_json(file_obj: BinaryIO, json_data: dict) -> None: data = json.dumps(json_data).encode("utf-8") compressed = compress_data(data) diff --git a/firefox/jsonlz4_mozLz4/test/test.py b/firefox/jsonlz4_mozLz4/test/test.py index a65eed66f..2179a9864 100644 --- a/firefox/jsonlz4_mozLz4/test/test.py +++ b/firefox/jsonlz4_mozLz4/test/test.py @@ -22,11 +22,11 @@ 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): + def test_decompress_compress(self) -> None: with open(FILE_TEST, "rb") as f: expected_data = mozlz4a.decompress(f) @@ -38,7 +38,7 @@ def test_decompress_compress(self): self.assertEqual(expected_data, data) - def test_compress_decompress_data(self): + def test_compress_decompress_data(self) -> None: expected_data = str(uuid.uuid4()).encode("utf-8") compressed_data = mozlz4a.compress_data(expected_data) @@ -46,7 +46,7 @@ def test_compress_decompress_data(self): self.assertEqual(expected_data, data) - def test_json(self): + def test_json(self) -> None: with open(FILE_TEST, "rb") as f: expected_json_data = mozlz4a.loads_json(f) diff --git a/flask-paginate__example/app.py b/flask-paginate__example/app.py index 73d99425f..57ed5fd7c 100644 --- a/flask-paginate__example/app.py +++ b/flask-paginate__example/app.py @@ -17,14 +17,14 @@ @app.before_request -def before_request(): +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): +def teardown(error) -> None: if hasattr(g, "conn"): g.conn.close() @@ -135,7 +135,7 @@ def get_pagination(**kwargs): @click.command() @click.option("--port", "-p", default=5000, help="listening port") -def run(port): +def run(port) -> None: app.run(debug=True, port=port) diff --git a/flask-paginate__example/sql.py b/flask-paginate__example/sql.py index 6364baef1..21fc60f4b 100644 --- a/flask-paginate__example/sql.py +++ b/flask-paginate__example/sql.py @@ -16,12 +16,12 @@ @click.group() -def cli(): +def cli() -> None: pass @cli.command(short_help="initialize database and tables") -def init_db(): +def init_db() -> None: conn = sqlite3.connect("test.db") cur = conn.cursor() cur.execute(sql) @@ -31,7 +31,7 @@ def init_db(): @cli.command(short_help="fill records to database") @click.option("--total", "-t", default=300, help="fill data for example") -def fill_data(total): +def fill_data(total) -> None: conn = sqlite3.connect("test.db") cur = conn.cursor() for i in range(total): diff --git a/flask__webservers/cors__hello_world.py b/flask__webservers/cors__hello_world.py index 533425d6f..f771cddde 100644 --- a/flask__webservers/cors__hello_world.py +++ b/flask__webservers/cors__hello_world.py @@ -16,7 +16,7 @@ @app.route("/") -def index(): +def index() -> str: return "Hello World!" 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 567468709..85a51fbea 100644 --- a/flask__webservers/flask_debugtoolbar__examples/app.py +++ b/flask__webservers/flask_debugtoolbar__examples/app.py @@ -28,7 +28,7 @@ class ExampleModel(db.Model): @app.before_first_request -def setup(): +def setup() -> None: db.create_all() diff --git a/flask__webservers/flask_debugtoolbar__examples/hello_world.py b/flask__webservers/flask_debugtoolbar__examples/hello_world.py index 58fbad365..d6846fb15 100644 --- a/flask__webservers/flask_debugtoolbar__examples/hello_world.py +++ b/flask__webservers/flask_debugtoolbar__examples/hello_world.py @@ -17,7 +17,7 @@ @app.route("/") -def index(): +def index() -> str: # NOTE: Need tab body: "Could not insert debug toolbar. tag not found in response." return "Hello World!" diff --git a/flask__webservers/get_URL__parameter_argument_query.py b/flask__webservers/get_URL__parameter_argument_query.py index 2038e8ad8..1145a004d 100644 --- a/flask__webservers/get_URL__parameter_argument_query.py +++ b/flask__webservers/get_URL__parameter_argument_query.py @@ -13,7 +13,7 @@ @app.route("/") -def index(): +def index() -> str: return """
diff --git a/flask__webservers/get_upload_and_image_process/main.py b/flask__webservers/get_upload_and_image_process/main.py index a62f2f7bc..6c7b48f64 100644 --- a/flask__webservers/get_upload_and_image_process/main.py +++ b/flask__webservers/get_upload_and_image_process/main.py @@ -35,7 +35,7 @@ # 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: @@ -72,12 +72,12 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): LAST_IMAGE = "last_image.jpg" -def save_last_image(file_data): +def save_last_image(file_data) -> None: with open(LAST_IMAGE, "wb") as f: f.write(file_data) -def load_last_image(): +def load_last_image() -> bytes: with open(LAST_IMAGE, "rb") as f: return f.read() diff --git a/flask__webservers/get_upload_image_info/main.py b/flask__webservers/get_upload_image_info/main.py index 20f5cc492..b1a2edacb 100644 --- a/flask__webservers/get_upload_image_info/main.py +++ b/flask__webservers/get_upload_image_info/main.py @@ -85,7 +85,7 @@ 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: diff --git a/flask__webservers/hello_world.py b/flask__webservers/hello_world.py index 0c8f7ba98..90f9c84e3 100644 --- a/flask__webservers/hello_world.py +++ b/flask__webservers/hello_world.py @@ -14,7 +14,7 @@ @app.route("/") -def index(): +def index() -> str: return "Hello World!" 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/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 d220702f3..6d22577a5 100644 --- a/flask__webservers/post_data.py +++ b/flask__webservers/post_data.py @@ -13,7 +13,7 @@ @app.route("/") -def index(): +def index() -> str: return """
diff --git a/flask__webservers/post_data__as_form.py b/flask__webservers/post_data__as_form.py index 8cb12bb16..ab732dd71 100644 --- a/flask__webservers/post_data__as_form.py +++ b/flask__webservers/post_data__as_form.py @@ -13,7 +13,7 @@ @app.route("/") -def index(): +def index() -> str: return """

diff --git a/flask__webservers/post_data__as_json.py b/flask__webservers/post_data__as_json.py index 5a312d5e3..3c44aa550 100644 --- a/flask__webservers/post_data__as_json.py +++ b/flask__webservers/post_data__as_json.py @@ -13,7 +13,7 @@ @app.route("/") -def index(): +def index() -> str: return """
diff --git a/flask__webservers/print_hex_post_data/main.py b/flask__webservers/print_hex_post_data/main.py index f3c94f9b7..e8b699b97 100644 --- a/flask__webservers/print_hex_post_data/main.py +++ b/flask__webservers/print_hex_post_data/main.py @@ -16,7 +16,7 @@ @app.route("/", methods=["POST"]) -def index(): +def index() -> str: data = request.data print(binascii.hexlify(data), data) diff --git a/flask__webservers/run_with_random_port/main.py b/flask__webservers/run_with_random_port/main.py index 56f3411aa..f9a8d2d53 100644 --- a/flask__webservers/run_with_random_port/main.py +++ b/flask__webservers/run_with_random_port/main.py @@ -13,7 +13,7 @@ @app.route("/") -def hello(): +def hello() -> str: return "Hello, world! running on %s" % request.host diff --git a/flask__webservers/server__handle_stdin__input.py b/flask__webservers/server__handle_stdin__input.py index 3abaef6fa..288ff43e8 100644 --- a/flask__webservers/server__handle_stdin__input.py +++ b/flask__webservers/server__handle_stdin__input.py @@ -20,7 +20,7 @@ text = "Hello World!" -def go(): +def go() -> None: time.sleep(2) print("\n") diff --git a/flask__webservers/server_with_additional_command_thread/main.py b/flask__webservers/server_with_additional_command_thread/main.py index 9b02595ba..fcb917e94 100644 --- a/flask__webservers/server_with_additional_command_thread/main.py +++ b/flask__webservers/server_with_additional_command_thread/main.py @@ -40,7 +40,7 @@ def img_search(): return "text: " + text -def loop_command_function(): +def loop_command_function() -> None: while True: global EXECUTE_COMMAND, COMMAND_TEXT diff --git a/flask__webservers/server_with_window_notification/main.py b/flask__webservers/server_with_window_notification/main.py index 57feff913..c7a16d49f 100644 --- a/flask__webservers/server_with_window_notification/main.py +++ b/flask__webservers/server_with_window_notification/main.py @@ -23,7 +23,7 @@ logging.basicConfig(level=logging.DEBUG) -def show(text): +def show(text) -> None: title = str(threading.current_thread()) run_in_thread(title, text, duration=20) @@ -34,7 +34,7 @@ def index(): @app.route("/show_notification") -def show_notification(): +def show_notification() -> str: text = request.args.get("text") print("text:", text) diff --git a/flask__webservers/show_lunch_menu_from_email/main.py b/flask__webservers/show_lunch_menu_from_email/main.py index a8635c7aa..6ebcd5323 100644 --- a/flask__webservers/show_lunch_menu_from_email/main.py +++ b/flask__webservers/show_lunch_menu_from_email/main.py @@ -67,7 +67,7 @@ def save_attachment(msg): return file_name -def add_lunch_email_info(msg, file_name): +def add_lunch_email_info(msg, file_name) -> None: """ Функция для добавления в поле comments docx информации о письме. diff --git a/flask__webservers/show_my_ip/main.py b/flask__webservers/show_my_ip/main.py index 88f4f3558..8ff891a59 100644 --- a/flask__webservers/show_my_ip/main.py +++ b/flask__webservers/show_my_ip/main.py @@ -13,7 +13,7 @@ @app.route("/") -def index(): +def index() -> str: return f"""Your IPv4 Address Is: {request.remote_addr}""" diff --git a/flask__webservers/simple_upload_server/main.py b/flask__webservers/simple_upload_server/main.py index 0eeb582f9..2f5faf9aa 100644 --- a/flask__webservers/simple_upload_server/main.py +++ b/flask__webservers/simple_upload_server/main.py @@ -11,7 +11,7 @@ @app.route("/", methods=["POST"]) -def index(): +def index() -> str: file = request.files["file"] # file.save(file.filename) # OR: 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 473254f09..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 @@ -10,7 +10,7 @@ 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}%)" @@ -19,7 +19,7 @@ def progress(count, block_size, total_size): ) -def create_test_file(): +def create_test_file() -> None: file_name = "uploads/bigfile" if os.path.exists(file_name): return diff --git a/flask__webservers/url_shortener/db.py b/flask__webservers/url_shortener/db.py index 37cfb91a8..2b8c3dc08 100644 --- a/flask__webservers/url_shortener/db.py +++ b/flask__webservers/url_shortener/db.py @@ -23,7 +23,7 @@ def generate_link_id(length: int = LENGTH_URL_ID) -> str: class NotDefinedParameterException(Exception): - def __init__(self, parameter_name: str): + def __init__(self, parameter_name: str) -> None: self.parameter_name = parameter_name text = f'Parameter "{self.parameter_name}" must be defined!' @@ -73,7 +73,7 @@ def get_inherited_models(cls) -> list[Type["BaseModel"]]: return sorted(cls.__subclasses__(), key=lambda x: x.__name__) @classmethod - def print_count_of_tables(cls): + def print_count_of_tables(cls) -> None: items = [] for sub_cls in cls.get_inherited_models(): name = sub_cls.__name__ @@ -89,7 +89,7 @@ def count(cls, filters: Iterable = None) -> int: query = query.filter(*filters) return query.count() - def __str__(self): + def __str__(self) -> str: fields = [] for k, field in self._meta.fields.items(): v = getattr(self, k) diff --git a/flask__webservers/users/main_with_db.py b/flask__webservers/users/main_with_db.py index 1a8a7a961..7f8fb28af 100644 --- a/flask__webservers/users/main_with_db.py +++ b/flask__webservers/users/main_with_db.py @@ -207,7 +207,7 @@ def logout(): @app.route("/protected") @flask_login.login_required -def protected(): +def protected() -> str: return f"Logged in as: {flask_login.current_user.username}" diff --git a/flask__webservers/users/main_without_db.py b/flask__webservers/users/main_without_db.py index a98b7223d..927caa1fd 100644 --- a/flask__webservers/users/main_without_db.py +++ b/flask__webservers/users/main_without_db.py @@ -119,7 +119,7 @@ def login(): @app.route("/protected") @flask_login.login_required -def protected(): +def protected() -> str: return f"Logged in as: {flask_login.current_user.id}" diff --git a/flask__websocket/ajax_vs_websocket/websocket/main.py b/flask__websocket/ajax_vs_websocket/websocket/main.py index 34a2f922d..f39e0d5b1 100644 --- a/flask__websocket/ajax_vs_websocket/websocket/main.py +++ b/flask__websocket/ajax_vs_websocket/websocket/main.py @@ -40,7 +40,7 @@ def index(): @socketio.on("post_method", namespace="/test") -def post_method(message): +def post_method(message) -> None: # print(message) with lock: diff --git a/flask__websocket/commands__websocket__flask-socketio/main.py b/flask__websocket/commands__websocket__flask-socketio/main.py index 0eeecb564..4324325e4 100644 --- a/flask__websocket/commands__websocket__flask-socketio/main.py +++ b/flask__websocket/commands__websocket__flask-socketio/main.py @@ -27,7 +27,7 @@ def index(): @socketio.on("my_event", namespace="/test") -def test_message(message): +def test_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 print(message) @@ -54,17 +54,17 @@ def test_message(message): @socketio.on("my_ping", namespace="/test") -def ping_pong(): +def ping_pong() -> None: emit("my_pong") @socketio.on("connect", namespace="/test") -def test_connect(): +def test_connect() -> None: emit("my_response", {"data": "Connected"}) @socketio.on("disconnect", namespace="/test") -def test_disconnect(): +def test_disconnect() -> None: print("Client disconnected", request.sid) diff --git a/flask__websocket/sending_all_from_while_in_thread/main.py b/flask__websocket/sending_all_from_while_in_thread/main.py index 814dd0a34..fce7334c0 100644 --- a/flask__websocket/sending_all_from_while_in_thread/main.py +++ b/flask__websocket/sending_all_from_while_in_thread/main.py @@ -25,7 +25,7 @@ socketio = SocketIO(app, async_mode=async_mode) -def send_all_cycled(): +def send_all_cycled() -> None: with app.app_context(): i = 0 while True: @@ -50,7 +50,7 @@ def index(): @socketio.on("my_event") -def test_message(message): +def test_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 print(message) @@ -63,18 +63,18 @@ def test_message(message): @socketio.on("my_ping") -def ping_pong(): +def ping_pong() -> None: emit("my_pong") @socketio.on("connect") -def test_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(): +def test_disconnect() -> None: print("Client disconnected", request.sid) diff --git a/flask__websocket/simple_chat/main.py b/flask__websocket/simple_chat/main.py index 3b39ff133..55928b994 100644 --- a/flask__websocket/simple_chat/main.py +++ b/flask__websocket/simple_chat/main.py @@ -26,7 +26,7 @@ def index(): @socketio.on("my_event", namespace="/test") -def test_message(message): +def test_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 print(message) @@ -39,17 +39,17 @@ def test_message(message): @socketio.on("my_ping", namespace="/test") -def ping_pong(): +def ping_pong() -> None: emit("my_pong") @socketio.on("connect", namespace="/test") -def test_connect(): +def test_connect() -> None: emit("my_response", {"data": f"Connected!", "count": 0, "sid": request.sid}) @socketio.on("disconnect", namespace="/test") -def test_disconnect(): +def test_disconnect() -> None: print("Client disconnected", request.sid) diff --git a/flask__websocket/websocket__flask-socketio_example/app.py b/flask__websocket/websocket__flask-socketio_example/app.py index d66496e44..526556795 100644 --- a/flask__websocket/websocket__flask-socketio_example/app.py +++ b/flask__websocket/websocket__flask-socketio_example/app.py @@ -27,7 +27,7 @@ thread_lock = Lock() -def background_thread(): +def background_thread() -> None: """Example of how to send server generated events to clients.""" count = 0 while True: @@ -46,7 +46,7 @@ def index(): @socketio.on("my_event", namespace="/test") -def test_message(message): +def test_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -55,7 +55,7 @@ def test_message(message): @socketio.on("my_broadcast_event", namespace="/test") -def test_broadcast_message(message): +def test_broadcast_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -65,7 +65,7 @@ def test_broadcast_message(message): @socketio.on("join", namespace="/test") -def join(message): +def join(message) -> None: join_room(message["room"]) session["receive_count"] = session.get("receive_count", 0) + 1 emit( @@ -75,7 +75,7 @@ def join(message): @socketio.on("leave", namespace="/test") -def leave(message): +def leave(message) -> None: leave_room(message["room"]) session["receive_count"] = session.get("receive_count", 0) + 1 emit( @@ -85,7 +85,7 @@ def leave(message): @socketio.on("close_room", namespace="/test") -def close(message): +def close(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -99,7 +99,7 @@ def close(message): @socketio.on("my_room_event", namespace="/test") -def send_room_message(message): +def send_room_message(message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -109,9 +109,9 @@ def send_room_message(message): @socketio.on("disconnect_request", namespace="/test") -def disconnect_request(): +def disconnect_request() -> None: @copy_current_request_context - def can_disconnect(): + def can_disconnect() -> None: disconnect() session["receive_count"] = session.get("receive_count", 0) + 1 @@ -126,12 +126,12 @@ def can_disconnect(): @socketio.on("my_ping", namespace="/test") -def ping_pong(): +def ping_pong() -> None: emit("my_pong") @socketio.on("connect", namespace="/test") -def test_connect(): +def test_connect() -> None: global thread with thread_lock: if thread is None: @@ -140,7 +140,7 @@ def test_connect(): @socketio.on("disconnect", namespace="/test") -def test_disconnect(): +def test_disconnect() -> None: print("Client disconnected", request.sid) diff --git a/flask__websocket/websocket__flask-socketio_example/app_namespace.py b/flask__websocket/websocket__flask-socketio_example/app_namespace.py index f7afbc120..8b8b9bf6d 100644 --- a/flask__websocket/websocket__flask-socketio_example/app_namespace.py +++ b/flask__websocket/websocket__flask-socketio_example/app_namespace.py @@ -27,7 +27,7 @@ thread_lock = Lock() -def background_thread(): +def background_thread() -> None: """Example of how to send server generated events to clients.""" count = 0 while True: @@ -46,14 +46,14 @@ def index(): class MyNamespace(Namespace): - def on_my_event(self, message): + def on_my_event(self, message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", {"data": message["data"], "count": session["receive_count"]}, ) - def on_my_broadcast_event(self, message): + def on_my_broadcast_event(self, message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -61,7 +61,7 @@ def on_my_broadcast_event(self, message): broadcast=True, ) - def on_join(self, message): + def on_join(self, message) -> None: join_room(message["room"]) session["receive_count"] = session.get("receive_count", 0) + 1 emit( @@ -72,7 +72,7 @@ def on_join(self, message): }, ) - def on_leave(self, message): + def on_leave(self, message) -> None: leave_room(message["room"]) session["receive_count"] = session.get("receive_count", 0) + 1 emit( @@ -83,7 +83,7 @@ def on_leave(self, message): }, ) - def on_close_room(self, message): + def on_close_room(self, message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -95,7 +95,7 @@ def on_close_room(self, message): ) close_room(message["room"]) - def on_my_room_event(self, message): + def on_my_room_event(self, message) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -103,7 +103,7 @@ def on_my_room_event(self, message): room=message["room"], ) - def on_disconnect_request(self): + def on_disconnect_request(self) -> None: session["receive_count"] = session.get("receive_count", 0) + 1 emit( "my_response", @@ -111,17 +111,17 @@ def on_disconnect_request(self): ) disconnect() - def on_my_ping(self): + def on_my_ping(self) -> None: emit("my_pong") - def on_connect(self): + def on_connect(self) -> None: global thread with thread_lock: if thread is None: thread = socketio.start_background_task(background_thread) emit("my_response", {"data": "Connected", "count": 0}) - def on_disconnect(self): + def on_disconnect(self) -> None: print("Client disconnected", request.sid) diff --git a/flask__websocket/websocket__flask-socketio_example/requirements.txt b/flask__websocket/websocket__flask-socketio_example/requirements.txt index 450d71da9..736c5fac4 100644 --- a/flask__websocket/websocket__flask-socketio_example/requirements.txt +++ b/flask__websocket/websocket__flask-socketio_example/requirements.txt @@ -8,4 +8,4 @@ MarkupSafe==1.1.0 python-engineio python-socketio six==1.11.0 -Werkzeug==2.2.3 +Werkzeug==3.1.6 diff --git a/flask__websocket/websocket__flask-socketio_example/sessions.py b/flask__websocket/websocket__flask-socketio_example/sessions.py index 9920d6b83..882e6749c 100644 --- a/flask__websocket/websocket__flask-socketio_example/sessions.py +++ b/flask__websocket/websocket__flask-socketio_example/sessions.py @@ -13,7 +13,7 @@ class User(UserMixin, object): - def __init__(self, id=None): + def __init__(self, id=None) -> None: self.id = id @@ -46,7 +46,7 @@ def session_access(): @socketio.on("get-session") -def get_session(): +def get_session() -> None: emit( "refresh-session", { @@ -57,7 +57,7 @@ def get_session(): @socketio.on("set-session") -def set_session(data): +def set_session(data) -> None: if "session" in data: session["value"] = data["session"] elif "user" in data: diff --git a/flask_http_auth/http_digest_auth.py b/flask_http_auth/http_digest_auth.py index db5c92ce8..c3eb060c9 100644 --- a/flask_http_auth/http_digest_auth.py +++ b/flask_http_auth/http_digest_auth.py @@ -34,7 +34,7 @@ def get_password(username: str) -> str | None: @app.route("/") @auth.login_required -def index(): +def index() -> str: return f"Hello, {auth.username()}!" diff --git a/flask_restful__examples/todo/main.py b/flask_restful__examples/todo/main.py index b1b3a647b..5ef9fa067 100644 --- a/flask_restful__examples/todo/main.py +++ b/flask_restful__examples/todo/main.py @@ -22,7 +22,7 @@ } -def abort_if_todo_doesnt_exist(todo_id): +def abort_if_todo_doesnt_exist(todo_id) -> None: if todo_id not in TODOS: abort(404, message=f"Todo {todo_id} doesn't exist") diff --git a/for_games/Binding of Isaac Rebirth/print_number_of_items.py b/for_games/Binding of Isaac Rebirth/print_number_of_items.py new file mode 100644 index 000000000..23afb4cfe --- /dev/null +++ b/for_games/Binding of Isaac Rebirth/print_number_of_items.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict + +import requests +from bs4 import BeautifulSoup + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0" + + +rs = session.get("https://isaac-items.ru/") +rs.raise_for_status() + +game_by_number: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + +soup = BeautifulSoup(rs.content, "html.parser") +for div_group in soup.select(".library-background > div"): + class_items = div_group.attrs.get("class", []) + class_str = str(class_items) + if "-container" not in class_str or "popup-container" in class_str: + continue + + is_rebirth = "rebirth" in class_items + is_afterbirth = "afterbirth" in class_items and "plus" not in class_items + is_afterbirth_plus = "afterbirth" in class_items and "plus" in class_items + is_repentance = "repentance" in class_items + + if "rebirth" in class_items: + game_name = "Rebirth" + elif "afterbirth" in class_items and "plus" not in class_items: + game_name = "Afterbirth" + elif "afterbirth" in class_items and "plus" in class_items: + game_name = "Afterbirth +" + elif "repentance" in class_items: + game_name = "Repentance" + else: + raise Exception(f"Unknown game by class: {class_items}") + + if "items-" in class_str: + category_name = "items" + elif "trinkets-" in class_str: + category_name = "trinkets" + elif "tarot-" in class_str: + category_name = "tarot" + else: + raise Exception(f"Unknown category by class: {class_items}") + + game_by_number[game_name][category_name] += len(div_group.select("li")) + + +print( + "Total items:", + sum( + sum(category_by_number.values()) + for category_by_number in game_by_number.values() + ), +) +print() + +for game, category_by_number in game_by_number.items(): + print(f"{game}:") + + for category, number in category_by_number.items(): + print(f" {category}: {number}") + + print() +""" +Total items: 1031 + +Repentance: + items: 173 + trinkets: 61 + +Rebirth: + items: 342 + trinkets: 60 + tarot: 123 + +Afterbirth: + items: 95 + trinkets: 29 + +Afterbirth +: + items: 110 + trinkets: 38 +""" diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses.json" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses.json" new file mode 100644 index 000000000..1ecc3f91f --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses.json" @@ -0,0 +1,192 @@ +{ + "ОСНОВНАЯ ИГРА. ОБЯЗАТЕЛЬНЫЕ БОССЫ": [ + { + "name": "Отец Гаскойн", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9E%D1%82%D0%B5%D1%86_%D0%93%D0%B0%D1%81%D0%BA%D0%BE%D0%B9%D0%BD" + }, + { + "name": "Викарий Амелия", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B8%D0%BA%D0%B0%D1%80%D0%B8%D0%B9_%D0%90%D0%BC%D0%B5%D0%BB%D0%B8%D1%8F" + }, + { + "name": "Тень Ярнама", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A2%D0%B5%D0%BD%D1%8C_%D0%AF%D1%80%D0%BD%D0%B0%D0%BC%D0%B0" + }, + { + "name": "Ром, праздный паук", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A0%D0%BE%D0%BC,_%D0%BF%D1%80%D0%B0%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B0%D1%83%D0%BA" + }, + { + "name": "Возродившийся", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%BE%D0%B7%D1%80%D0%BE%D0%B4%D0%B8%D0%B2%D1%88%D0%B8%D0%B9%D1%81%D1%8F" + }, + { + "name": "Миколаш, Хозяин кошмара", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D1%88,_%D0%A5%D0%BE%D0%B7%D1%8F%D0%B8%D0%BD_%D0%BA%D0%BE%D1%88%D0%BC%D0%B0%D1%80%D0%B0" + }, + { + "name": "Кормилица Мерго", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%80%D0%BC%D0%B8%D0%BB%D0%B8%D1%86%D0%B0_%D0%9C%D0%B5%D1%80%D0%B3%D0%BE" + } + ], + "ОСНОВНАЯ ИГРА. ОПЦИОНАЛЬНЫЕ БОССЫ": [ + { + "name": "Церковное Чудовище", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A6%D0%B5%D1%80%D0%BA%D0%BE%D0%B2%D0%BD%D0%BE%D0%B5_%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5" + }, + { + "name": "Посланник Небес", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%81%D0%BB%D0%B0%D0%BD%D0%BD%D0%B8%D0%BA_%D0%9D%D0%B5%D0%B1%D0%B5%D1%81" + }, + { + "name": "Ибраитас, дочь Космоса", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%98%D0%B1%D1%80%D0%B0%D0%B8%D1%82%D0%B0%D1%81,_%D0%B4%D0%BE%D1%87%D1%8C_%D0%9A%D0%BE%D1%81%D0%BC%D0%BE%D1%81%D0%B0" + }, + { + "name": "Чудовище-кровоглот", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82" + }, + { + "name": "Черное чудовище Паарл", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9F%D0%B0%D0%B0%D1%80%D0%BB" + }, + { + "name": "Ведьма Хемвика", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B5%D0%B4%D1%8C%D0%BC%D0%B0_%D0%A5%D0%B5%D0%BC%D0%B2%D0%B8%D0%BA%D0%B0" + }, + { + "name": "Мученик Логариус", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%BA_%D0%9B%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%83%D1%81" + }, + { + "name": "Амигдала", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%90%D0%BC%D0%B8%D0%B3%D0%B4%D0%B0%D0%BB%D0%B0" + }, + { + "name": "Герман, первый охотник", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%93%D0%B5%D1%80%D0%BC%D0%B0%D0%BD,_%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B9_%D0%BE%D1%85%D0%BE%D1%82%D0%BD%D0%B8%D0%BA" + }, + { + "name": "Присутствие луны", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D1%80%D0%B8%D1%81%D1%83%D1%82%D1%81%D1%82%D0%B2%D0%B8%D0%B5_%D0%BB%D1%83%D0%BD%D1%8B" + } + ], + "ДОПОЛНЕНИЕ THE OLD HUNTERS": [ + { + "name": "Людвиг", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D1%8E%D0%B4%D0%B2%D0%B8%D0%B3" + }, + { + "name": "Лоуренс, первый викарий", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%BE%D1%83%D1%80%D0%B5%D0%BD%D1%81,_%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B9_%D0%B2%D0%B8%D0%BA%D0%B0%D1%80%D0%B8%D0%B9" + }, + { + "name": "Живые неудачи", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%96%D0%B8%D0%B2%D1%8B%D0%B5_%D0%BD%D0%B5%D1%83%D0%B4%D0%B0%D1%87%D0%B8" + }, + { + "name": "Леди Мария из Астральной часовой башни", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%B5%D0%B4%D0%B8_%D0%9C%D0%B0%D1%80%D0%B8%D1%8F_%D0%B8%D0%B7_%D0%90%D1%81%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D1%87%D0%B0%D1%81%D0%BE%D0%B2%D0%BE%D0%B9_%D0%B1%D0%B0%D1%88%D0%BD%D0%B8" + }, + { + "name": "Сирота Кос", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B8%D1%80%D0%BE%D1%82%D0%B0_%D0%9A%D0%BE%D1%81" + } + ], + "ОБЛАСТЬ ПТУМЕРУ": [ + { + "name": "Мертвый гигант", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B5%D1%80%D1%82%D0%B2%D1%8B%D0%B9_%D0%B3%D0%B8%D0%B3%D0%B0%D0%BD%D1%82" + }, + { + "name": "Беспощадные Хранители", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%91%D0%B5%D1%81%D0%BF%D0%BE%D1%89%D0%B0%D0%B4%D0%BD%D1%8B%D0%B5_%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D0%B8" + }, + { + "name": "Сторожевой пес Древних Богов", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D1%82%D0%BE%D1%80%D0%BE%D0%B6%D0%B5%D0%B2%D0%BE%D0%B9_%D0%BF%D0%B5%D1%81_%D0%94%D1%80%D0%B5%D0%B2%D0%BD%D0%B8%D1%85_%D0%91%D0%BE%D0%B3%D0%BE%D0%B2" + }, + { + "name": "Душа, одержимая чудовищем", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%94%D1%83%D1%88%D0%B0,_%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%BC%D0%B0%D1%8F_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5%D0%BC" + }, + { + "name": "Хранитель Древних Богов", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_%D0%94%D1%80%D0%B5%D0%B2%D0%BD%D0%B8%D1%85_%D0%91%D0%BE%D0%B3%D0%BE%D0%B2" + }, + { + "name": "Потомок птумериан", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%82%D0%BE%D0%BC%D0%BE%D0%BA_%D0%BF%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD" + }, + { + "name": "Ром, праздный паук", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A0%D0%BE%D0%BC,_%D0%BF%D1%80%D0%B0%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B0%D1%83%D0%BA" + }, + { + "name": "Чудовище-кровопуск", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%BF%D1%83%D1%81%D0%BA" + }, + { + "name": "Амигдала", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%90%D0%BC%D0%B8%D0%B3%D0%B4%D0%B0%D0%BB%D0%B0" + }, + { + "name": "Ярнам, Птумерианская королева", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%AF%D1%80%D0%BD%D0%B0%D0%BC,_%D0%9F%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D0%B5%D0%B2%D0%B0" + } + ], + "ОБЛАСТЬ ДАЛЬНИХ ГРОБНИЦ": [ + { + "name": "Вепрь-людоед", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B5%D0%BF%D1%80%D1%8C-%D0%BB%D1%8E%D0%B4%D0%BE%D0%B5%D0%B4" + }, + { + "name": "Чудовище-кровоглот", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82" + }, + { + "name": "Мозгосос", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%BE%D0%B7%D0%B3%D0%BE%D1%81%D0%BE%D1%81" + }, + { + "name": "Забытый безумец", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%B1%D1%8B%D1%82%D1%8B%D0%B9_%D0%B1%D0%B5%D0%B7%D1%83%D0%BC%D0%B5%D1%86" + }, + { + "name": "Птумерианский Старейшина", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD%D1%81%D0%BA%D0%B8%D0%B9_%D0%A1%D1%82%D0%B0%D1%80%D0%B5%D0%B9%D1%88%D0%B8%D0%BD%D0%B0" + } + ], + "ОБЛАСТЬ ЛОРАНА": [ + { + "name": "Чудовище-кровоглот", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82" + }, + { + "name": "Отвратительное чудовище", + "url": "https://bloodborne.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%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5" + }, + { + "name": "Серебряное чудовище Лорана", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D1%8F%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9B%D0%BE%D1%80%D0%B0%D0%BD%D0%B0" + }, + { + "name": "Черное чудовище Паарл", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9F%D0%B0%D0%B0%D1%80%D0%BB" + } + ], + "ОБЛАСТЬ ИСЗА": [ + { + "name": "Мозгосос", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%BE%D0%B7%D0%B3%D0%BE%D1%81%D0%BE%D1%81" + }, + { + "name": "Посланник Небес", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%81%D0%BB%D0%B0%D0%BD%D0%BD%D0%B8%D0%BA_%D0%9D%D0%B5%D0%B1%D0%B5%D1%81" + }, + { + "name": "Ибраитас, дочь Космоса", + "url": "https://bloodborne.fandom.com/ru/wiki/%D0%98%D0%B1%D1%80%D0%B0%D0%B8%D1%82%D0%B0%D1%81,_%D0%B4%D0%BE%D1%87%D1%8C_%D0%9A%D0%BE%D1%81%D0%BC%D0%BE%D1%81%D0%B0" + } + ] +} \ No newline at end of file diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.json" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.json" new file mode 100644 index 000000000..5bc4c2ef5 --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.json" @@ -0,0 +1,60 @@ +{ + "ОСНОВНАЯ ИГРА. ОБЯЗАТЕЛЬНЫЕ БОССЫ": [ + "Отец Гаскойн", + "Викарий Амелия", + "Тень Ярнама", + "Ром, праздный паук", + "Возродившийся", + "Миколаш, Хозяин кошмара", + "Кормилица Мерго" + ], + "ОСНОВНАЯ ИГРА. ОПЦИОНАЛЬНЫЕ БОССЫ": [ + "Церковное Чудовище", + "Посланник Небес", + "Ибраитас, дочь Космоса", + "Чудовище-кровоглот", + "Черное чудовище Паарл", + "Ведьма Хемвика", + "Мученик Логариус", + "Амигдала", + "Герман, первый охотник", + "Присутствие луны" + ], + "ДОПОЛНЕНИЕ THE OLD HUNTERS": [ + "Людвиг", + "Лоуренс, первый викарий", + "Живые неудачи", + "Леди Мария из Астральной часовой башни", + "Сирота Кос" + ], + "ОБЛАСТЬ ПТУМЕРУ": [ + "Мертвый гигант", + "Беспощадные Хранители", + "Сторожевой пес Древних Богов", + "Душа, одержимая чудовищем", + "Хранитель Древних Богов", + "Потомок птумериан", + "Ром, праздный паук", + "Чудовище-кровопуск", + "Амигдала", + "Ярнам, Птумерианская королева" + ], + "ОБЛАСТЬ ДАЛЬНИХ ГРОБНИЦ": [ + "Вепрь-людоед", + "Чудовище-кровоглот", + "Мозгосос", + "Забытый безумец", + "Птумерианский Старейшина" + ], + "ОБЛАСТЬ ЛОРАНА": [ + "Чудовище-кровоглот", + "Отвратительное чудовище", + "Серебряное чудовище Лорана", + "Черное чудовище Паарл" + ], + "ОБЛАСТЬ ИСЗА": [ + "Мозгосос", + "Посланник Небес", + "Ибраитас, дочь Космоса" + ] +} \ No newline at end of file diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.txt" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.txt" new file mode 100644 index 000000000..9c5dcfc04 --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/dumps/bosses__only_name.txt" @@ -0,0 +1,57 @@ +ОСНОВНАЯ ИГРА. ОБЯЗАТЕЛЬНЫЕ БОССЫ + Отец Гаскойн + Викарий Амелия + Тень Ярнама + Ром, праздный паук + Возродившийся + Миколаш, Хозяин кошмара + Кормилица Мерго + +ОСНОВНАЯ ИГРА. ОПЦИОНАЛЬНЫЕ БОССЫ + Церковное Чудовище + Посланник Небес + Ибраитас, дочь Космоса + Чудовище-кровоглот + Черное чудовище Паарл + Ведьма Хемвика + Мученик Логариус + Амигдала + Герман, первый охотник + Присутствие луны + +ДОПОЛНЕНИЕ THE OLD HUNTERS + Людвиг + Лоуренс, первый викарий + Живые неудачи + Леди Мария из Астральной часовой башни + Сирота Кос + +ОБЛАСТЬ ПТУМЕРУ + Мертвый гигант + Беспощадные Хранители + Сторожевой пес Древних Богов + Душа, одержимая чудовищем + Хранитель Древних Богов + Потомок птумериан + Ром, праздный паук + Чудовище-кровопуск + Амигдала + Ярнам, Птумерианская королева + +ОБЛАСТЬ ДАЛЬНИХ ГРОБНИЦ + Вепрь-людоед + Чудовище-кровоглот + Мозгосос + Забытый безумец + Птумерианский Старейшина + +ОБЛАСТЬ ЛОРАНА + Чудовище-кровоглот + Отвратительное чудовище + Серебряное чудовище Лорана + Черное чудовище Паарл + +ОБЛАСТЬ ИСЗА + Мозгосос + Посланник Небес + Ибраитас, дочь Космоса diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" new file mode 100644 index 000000000..a84dade8d --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json + +from collections import defaultdict +from dataclasses import dataclass, asdict +from pathlib import Path +from urllib.parse import urljoin + +from bs4 import BeautifulSoup +import requests + + +@dataclass +class Boss: + name: str + url: str + + +URL: str = "https://bloodborne.fandom.com/ru/wiki/Боссы" + + +def get_bosses() -> dict[str, list[Boss]]: + rs = requests.get(URL) + rs.raise_for_status() + + soup = BeautifulSoup(rs.content, "html.parser") + + bosses_by_category: dict[str, list[Boss]] = defaultdict(list) + category_name: str | None = None + + for tr in soup.select("table.article-table tr"): + # Заголовок первым идет + th = tr.select_one("th") + if th: + category_name = th.text.strip().upper() + continue + + if not category_name: + continue + + bosses_by_category[category_name] += [ + Boss( + name=a["title"], + url=urljoin(rs.url, a["href"]), + ) + for a in tr.select("td > figure > a[href][title]") + ] + + return bosses_by_category + + +def print_bosses(bosses: dict[str, list[Boss]]) -> None: + total = sum(len(i) for i in bosses.values()) + print(f"Боссы ({total}):") + + 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() + + +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: Path, bosses: dict[str, list[Boss | str]]) -> None: + file_name.parent.mkdir(parents=True, exist_ok=True) + + json.dump( + bosses, + open(file_name, "w", encoding="utf-8"), + ensure_ascii=False, + indent=4, + default=asdict, + ) + + +def export_to_simple_text(file_name, bosses: dict[str, list[Boss]]) -> None: + file_name.parent.mkdir(parents=True, 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__": + DIR: Path = Path(__file__).resolve().parent + + bosses: dict[str, list[Boss]] = get_bosses() + print("Всего боссов:", sum(len(i) for i in bosses.values())) + # Всего боссов: 44 + + print() + + print_bosses(bosses) + """ + Боссы (44): + ОСНОВНАЯ ИГРА. ОБЯЗАТЕЛЬНЫЕ БОССЫ (7): + 1. "Отец Гаскойн": https://bloodborne.fandom.com/ru/wiki/%D0%9E%D1%82%D0%B5%D1%86_%D0%93%D0%B0%D1%81%D0%BA%D0%BE%D0%B9%D0%BD + 2. "Викарий Амелия": https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B8%D0%BA%D0%B0%D1%80%D0%B8%D0%B9_%D0%90%D0%BC%D0%B5%D0%BB%D0%B8%D1%8F + 3. "Тень Ярнама": https://bloodborne.fandom.com/ru/wiki/%D0%A2%D0%B5%D0%BD%D1%8C_%D0%AF%D1%80%D0%BD%D0%B0%D0%BC%D0%B0 + 4. "Ром, праздный паук": https://bloodborne.fandom.com/ru/wiki/%D0%A0%D0%BE%D0%BC,_%D0%BF%D1%80%D0%B0%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B0%D1%83%D0%BA + 5. "Возродившийся": https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%BE%D0%B7%D1%80%D0%BE%D0%B4%D0%B8%D0%B2%D1%88%D0%B8%D0%B9%D1%81%D1%8F + 6. "Миколаш, Хозяин кошмара": https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D1%88,_%D0%A5%D0%BE%D0%B7%D1%8F%D0%B8%D0%BD_%D0%BA%D0%BE%D1%88%D0%BC%D0%B0%D1%80%D0%B0 + 7. "Кормилица Мерго": https://bloodborne.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%80%D0%BC%D0%B8%D0%BB%D0%B8%D1%86%D0%B0_%D0%9C%D0%B5%D1%80%D0%B3%D0%BE + + ОСНОВНАЯ ИГРА. ОПЦИОНАЛЬНЫЕ БОССЫ (10): + 1. "Церковное Чудовище": https://bloodborne.fandom.com/ru/wiki/%D0%A6%D0%B5%D1%80%D0%BA%D0%BE%D0%B2%D0%BD%D0%BE%D0%B5_%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5 + 2. "Посланник Небес": https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%81%D0%BB%D0%B0%D0%BD%D0%BD%D0%B8%D0%BA_%D0%9D%D0%B5%D0%B1%D0%B5%D1%81 + 3. "Ибраитас, дочь Космоса": https://bloodborne.fandom.com/ru/wiki/%D0%98%D0%B1%D1%80%D0%B0%D0%B8%D1%82%D0%B0%D1%81,_%D0%B4%D0%BE%D1%87%D1%8C_%D0%9A%D0%BE%D1%81%D0%BC%D0%BE%D1%81%D0%B0 + 4. "Чудовище-кровоглот": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82 + 5. "Черное чудовище Паарл": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9F%D0%B0%D0%B0%D1%80%D0%BB + 6. "Ведьма Хемвика": https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B5%D0%B4%D1%8C%D0%BC%D0%B0_%D0%A5%D0%B5%D0%BC%D0%B2%D0%B8%D0%BA%D0%B0 + 7. "Мученик Логариус": https://bloodborne.fandom.com/ru/wiki/%D0%9C%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%BA_%D0%9B%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%83%D1%81 + 8. "Амигдала": https://bloodborne.fandom.com/ru/wiki/%D0%90%D0%BC%D0%B8%D0%B3%D0%B4%D0%B0%D0%BB%D0%B0 + 9. "Герман, первый охотник": https://bloodborne.fandom.com/ru/wiki/%D0%93%D0%B5%D1%80%D0%BC%D0%B0%D0%BD,_%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B9_%D0%BE%D1%85%D0%BE%D1%82%D0%BD%D0%B8%D0%BA + 10. "Присутствие луны": https://bloodborne.fandom.com/ru/wiki/%D0%9F%D1%80%D0%B8%D1%81%D1%83%D1%82%D1%81%D1%82%D0%B2%D0%B8%D0%B5_%D0%BB%D1%83%D0%BD%D1%8B + + ДОПОЛНЕНИЕ THE OLD HUNTERS (5): + 1. "Людвиг": https://bloodborne.fandom.com/ru/wiki/%D0%9B%D1%8E%D0%B4%D0%B2%D0%B8%D0%B3 + 2. "Лоуренс, первый викарий": https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%BE%D1%83%D1%80%D0%B5%D0%BD%D1%81,_%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B9_%D0%B2%D0%B8%D0%BA%D0%B0%D1%80%D0%B8%D0%B9 + 3. "Живые неудачи": https://bloodborne.fandom.com/ru/wiki/%D0%96%D0%B8%D0%B2%D1%8B%D0%B5_%D0%BD%D0%B5%D1%83%D0%B4%D0%B0%D1%87%D0%B8 + 4. "Леди Мария из Астральной часовой башни": https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%B5%D0%B4%D0%B8_%D0%9C%D0%B0%D1%80%D0%B8%D1%8F_%D0%B8%D0%B7_%D0%90%D1%81%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D1%87%D0%B0%D1%81%D0%BE%D0%B2%D0%BE%D0%B9_%D0%B1%D0%B0%D1%88%D0%BD%D0%B8 + 5. "Сирота Кос": https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B8%D1%80%D0%BE%D1%82%D0%B0_%D0%9A%D0%BE%D1%81 + + ОБЛАСТЬ ПТУМЕРУ (10): + 1. "Мертвый гигант": https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B5%D1%80%D1%82%D0%B2%D1%8B%D0%B9_%D0%B3%D0%B8%D0%B3%D0%B0%D0%BD%D1%82 + 2. "Беспощадные Хранители": https://bloodborne.fandom.com/ru/wiki/%D0%91%D0%B5%D1%81%D0%BF%D0%BE%D1%89%D0%B0%D0%B4%D0%BD%D1%8B%D0%B5_%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D0%B8 + 3. "Сторожевой пес Древних Богов": https://bloodborne.fandom.com/ru/wiki/%D0%A1%D1%82%D0%BE%D1%80%D0%BE%D0%B6%D0%B5%D0%B2%D0%BE%D0%B9_%D0%BF%D0%B5%D1%81_%D0%94%D1%80%D0%B5%D0%B2%D0%BD%D0%B8%D1%85_%D0%91%D0%BE%D0%B3%D0%BE%D0%B2 + 4. "Душа, одержимая чудовищем": https://bloodborne.fandom.com/ru/wiki/%D0%94%D1%83%D1%88%D0%B0,_%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%BC%D0%B0%D1%8F_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5%D0%BC + 5. "Хранитель Древних Богов": https://bloodborne.fandom.com/ru/wiki/%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_%D0%94%D1%80%D0%B5%D0%B2%D0%BD%D0%B8%D1%85_%D0%91%D0%BE%D0%B3%D0%BE%D0%B2 + 6. "Потомок птумериан": https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%82%D0%BE%D0%BC%D0%BE%D0%BA_%D0%BF%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD + 7. "Ром, праздный паук": https://bloodborne.fandom.com/ru/wiki/%D0%A0%D0%BE%D0%BC,_%D0%BF%D1%80%D0%B0%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B0%D1%83%D0%BA + 8. "Чудовище-кровопуск": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%BF%D1%83%D1%81%D0%BA + 9. "Амигдала": https://bloodborne.fandom.com/ru/wiki/%D0%90%D0%BC%D0%B8%D0%B3%D0%B4%D0%B0%D0%BB%D0%B0 + 10. "Ярнам, Птумерианская королева": https://bloodborne.fandom.com/ru/wiki/%D0%AF%D1%80%D0%BD%D0%B0%D0%BC,_%D0%9F%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D0%B5%D0%B2%D0%B0 + + ОБЛАСТЬ ДАЛЬНИХ ГРОБНИЦ (5): + 1. "Вепрь-людоед": https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B5%D0%BF%D1%80%D1%8C-%D0%BB%D1%8E%D0%B4%D0%BE%D0%B5%D0%B4 + 2. "Чудовище-кровоглот": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82 + 3. "Мозгосос": https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%BE%D0%B7%D0%B3%D0%BE%D1%81%D0%BE%D1%81 + 4. "Забытый безумец": https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%B1%D1%8B%D1%82%D1%8B%D0%B9_%D0%B1%D0%B5%D0%B7%D1%83%D0%BC%D0%B5%D1%86 + 5. "Птумерианский Старейшина": https://bloodborne.fandom.com/ru/wiki/%D0%9F%D1%82%D1%83%D0%BC%D0%B5%D1%80%D0%B8%D0%B0%D0%BD%D1%81%D0%BA%D0%B8%D0%B9_%D0%A1%D1%82%D0%B0%D1%80%D0%B5%D0%B9%D1%88%D0%B8%D0%BD%D0%B0 + + ОБЛАСТЬ ЛОРАНА (4): + 1. "Чудовище-кровоглот": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5-%D0%BA%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BB%D0%BE%D1%82 + 2. "Отвратительное чудовище": https://bloodborne.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%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5 + 3. "Серебряное чудовище Лорана": https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D1%8F%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9B%D0%BE%D1%80%D0%B0%D0%BD%D0%B0 + 4. "Черное чудовище Паарл": https://bloodborne.fandom.com/ru/wiki/%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B5_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B5_%D0%9F%D0%B0%D0%B0%D1%80%D0%BB + + ОБЛАСТЬ ИСЗА (3): + 1. "Мозгосос": https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%BE%D0%B7%D0%B3%D0%BE%D1%81%D0%BE%D1%81 + 2. "Посланник Небес": https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D1%81%D0%BB%D0%B0%D0%BD%D0%BD%D0%B8%D0%BA_%D0%9D%D0%B5%D0%B1%D0%B5%D1%81 + 3. "Ибраитас, дочь Космоса": https://bloodborne.fandom.com/ru/wiki/%D0%98%D0%B1%D1%80%D0%B0%D0%B8%D1%82%D0%B0%D1%81,_%D0%B4%D0%BE%D1%87%D1%8C_%D0%9A%D0%BE%D1%81%D0%BC%D0%BE%D1%81%D0%B0 + + """ + + export_to_json(DIR / "dumps/bosses.json", bosses) + export_to_json( + DIR / "dumps/bosses__only_name.json", convert_bosses_to_only_name(bosses) + ) + export_to_simple_text(DIR / "dumps/bosses__only_name.txt", bosses) diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations.json" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations.json" new file mode 100644 index 000000000..2f871ea0d --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations.json" @@ -0,0 +1,46 @@ +{ + "Алтарь отчаяния": "https://bloodborne.fandom.com/ru/wiki/%D0%90%D0%BB%D1%82%D0%B0%D1%80%D1%8C_%D0%BE%D1%82%D1%87%D0%B0%D1%8F%D0%BD%D0%B8%D1%8F", + "Астральная часовая башня": "https://bloodborne.fandom.com/ru/wiki/%D0%90%D1%81%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%87%D0%B0%D1%81%D0%BE%D0%B2%D0%B0%D1%8F_%D0%B1%D0%B0%D1%88%D0%BD%D1%8F", + "Большой мост": "https://bloodborne.fandom.com/ru/wiki/%D0%91%D0%BE%D0%BB%D1%8C%D1%88%D0%BE%D0%B9_%D0%BC%D0%BE%D1%81%D1%82", + "Бюргенверт": "https://bloodborne.fandom.com/ru/wiki/%D0%91%D1%8E%D1%80%D0%B3%D0%B5%D0%BD%D0%B2%D0%B5%D1%80%D1%82", + "Верхний Соборный округ": "https://bloodborne.fandom.com/ru/wiki/%D0%92%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9_%D0%A1%D0%BE%D0%B1%D0%BE%D1%80%D0%BD%D1%8B%D0%B9_%D0%BE%D0%BA%D1%80%D1%83%D0%B3", + "Главный собор": "https://bloodborne.fandom.com/ru/wiki/%D0%93%D0%BB%D0%B0%D0%B2%D0%BD%D1%8B%D0%B9_%D1%81%D0%BE%D0%B1%D0%BE%D1%80", + "Главный собор кошмара": "https://bloodborne.fandom.com/ru/wiki/%D0%93%D0%BB%D0%B0%D0%B2%D0%BD%D1%8B%D0%B9_%D1%81%D0%BE%D0%B1%D0%BE%D1%80_%D0%BA%D0%BE%D1%88%D0%BC%D0%B0%D1%80%D0%B0", + "Граница кошмара": "https://bloodborne.fandom.com/ru/wiki/%D0%93%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0_%D0%BA%D0%BE%D1%88%D0%BC%D0%B0%D1%80%D0%B0", + "Гробница Идон": "https://bloodborne.fandom.com/ru/wiki/%D0%93%D1%80%D0%BE%D0%B1%D0%BD%D0%B8%D1%86%D0%B0_%D0%98%D0%B4%D0%BE%D0%BD", + "Зал Амигдалы": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%BB_%D0%90%D0%BC%D0%B8%D0%B3%D0%B4%D0%B0%D0%BB%D1%8B", + "Зал исследований": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%BB_%D0%B8%D1%81%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B9", + "Зал королевы Нечистокровных": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%BB_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D0%B5%D0%B2%D1%8B_%D0%9D%D0%B5%D1%87%D0%B8%D1%81%D1%82%D0%BE%D0%BA%D1%80%D0%BE%D0%B2%D0%BD%D1%8B%D1%85", + "Запретная могила": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%BF%D1%80%D0%B5%D1%82%D0%BD%D0%B0%D1%8F_%D0%BC%D0%BE%D0%B3%D0%B8%D0%BB%D0%B0", + "Запретный лес": "https://bloodborne.fandom.com/ru/wiki/%D0%97%D0%B0%D0%BF%D1%80%D0%B5%D1%82%D0%BD%D1%8B%D0%B9_%D0%BB%D0%B5%D1%81", + "Кладбище Черного чудовища": "https://bloodborne.fandom.com/ru/wiki/%D0%9A%D0%BB%D0%B0%D0%B4%D0%B1%D0%B8%D1%89%D0%B5_%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B3%D0%BE_%D1%87%D1%83%D0%B4%D0%BE%D0%B2%D0%B8%D1%89%D0%B0", + "Кошмар Менсиса": "https://bloodborne.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%88%D0%BC%D0%B0%D1%80_%D0%9C%D0%B5%D0%BD%D1%81%D0%B8%D1%81%D0%B0", + "Кошмар охотника": "https://bloodborne.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%88%D0%BC%D0%B0%D1%80_%D0%BE%D1%85%D0%BE%D1%82%D0%BD%D0%B8%D0%BA%D0%B0", + "Лекционный корпус": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%B5%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D1%80%D0%BF%D1%83%D1%81", + "Лечебница Йозефки": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D0%B5%D1%87%D0%B5%D0%B1%D0%BD%D0%B8%D1%86%D0%B0_%D0%99%D0%BE%D0%B7%D0%B5%D1%84%D0%BA%D0%B8", + "Лунарий Кормилицы Мерго": "https://bloodborne.fandom.com/ru/wiki/%D0%9B%D1%83%D0%BD%D0%B0%D1%80%D0%B8%D0%B9_%D0%9A%D0%BE%D1%80%D0%BC%D0%B8%D0%BB%D0%B8%D1%86%D1%8B_%D0%9C%D0%B5%D1%80%D0%B3%D0%BE", + "Мастерская Церкви исцеления": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B0%D1%81%D1%82%D0%B5%D1%80%D1%81%D0%BA%D0%B0%D1%8F_%D0%A6%D0%B5%D1%80%D0%BA%D0%B2%D0%B8_%D0%B8%D1%81%D1%86%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F", + "Место Логариуса": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%B5%D1%81%D1%82%D0%BE_%D0%9B%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%83%D1%81%D0%B0", + "Молельный переулок Хемвик": "https://bloodborne.fandom.com/ru/wiki/%D0%9C%D0%BE%D0%BB%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B5%D1%80%D0%B5%D1%83%D0%BB%D0%BE%D0%BA_%D0%A5%D0%B5%D0%BC%D0%B2%D0%B8%D0%BA", + "Обитель ведьмы": "https://bloodborne.fandom.com/ru/wiki/%D0%9E%D0%B1%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_%D0%B2%D0%B5%D0%B4%D1%8C%D0%BC%D1%8B", + "Озеро лунного отражения": "https://bloodborne.fandom.com/ru/wiki/%D0%9E%D0%B7%D0%B5%D1%80%D0%BE_%D0%BB%D1%83%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BE%D1%82%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F", + "Площадь Пришествия": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BB%D0%BE%D1%89%D0%B0%D0%B4%D1%8C_%D0%9F%D1%80%D0%B8%D1%88%D0%B5%D1%81%D1%82%D0%B2%D0%B8%D1%8F", + "Побережье": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D0%B1%D0%B5%D1%80%D0%B5%D0%B6%D1%8C%D0%B5", + "Подземелье чаши": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D0%B4%D0%B7%D0%B5%D0%BC%D0%B5%D0%BB%D1%8C%D0%B5_%D1%87%D0%B0%D1%88%D0%B8", + "Подземная куча трупов": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D0%B4%D0%B7%D0%B5%D0%BC%D0%BD%D0%B0%D1%8F_%D0%BA%D1%83%D1%87%D0%B0_%D1%82%D1%80%D1%83%D0%BF%D0%BE%D0%B2", + "Подземная тюрьма": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D0%B4%D0%B7%D0%B5%D0%BC%D0%BD%D0%B0%D1%8F_%D1%82%D1%8E%D1%80%D1%8C%D0%BC%D0%B0", + "Покинутый замок Кейнхёрст": "https://bloodborne.fandom.com/ru/wiki/%D0%9F%D0%BE%D0%BA%D0%B8%D0%BD%D1%83%D1%82%D1%8B%D0%B9_%D0%B7%D0%B0%D0%BC%D0%BE%D0%BA_%D0%9A%D0%B5%D0%B9%D0%BD%D1%85%D1%91%D1%80%D1%81%D1%82", + "Рыбацкая деревня": "https://bloodborne.fandom.com/ru/wiki/%D0%A0%D1%8B%D0%B1%D0%B0%D1%86%D0%BA%D0%B0%D1%8F_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BD%D1%8F", + "Сад светящегося дерева": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B0%D0%B4_%D1%81%D0%B2%D0%B5%D1%82%D1%8F%D1%89%D0%B5%D0%B3%D0%BE%D1%81%D1%8F_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0", + "Сады светящихся цветов": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%B0%D0%B4%D1%8B_%D1%81%D0%B2%D0%B5%D1%82%D1%8F%D1%89%D0%B8%D1%85%D1%81%D1%8F_%D1%86%D0%B2%D0%B5%D1%82%D0%BE%D0%B2", + "Соборный округ": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%BE%D0%B1%D0%BE%D1%80%D0%BD%D1%8B%D0%B9_%D0%BE%D0%BA%D1%80%D1%83%D0%B3", + "Сон охотника": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D0%BE%D0%BD_%D0%BE%D1%85%D0%BE%D1%82%D0%BD%D0%B8%D0%BA%D0%B0", + "Старая заброшенная мастерская": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D1%80%D0%B0%D1%8F_%D0%B7%D0%B0%D0%B1%D1%80%D0%BE%D1%88%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_%D0%BC%D0%B0%D1%81%D1%82%D0%B5%D1%80%D1%81%D0%BA%D0%B0%D1%8F", + "Старый Ярнам": "https://bloodborne.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D1%80%D1%8B%D0%B9_%D0%AF%D1%80%D0%BD%D0%B0%D0%BC", + "Хижина рядом с маяком": "https://bloodborne.fandom.com/ru/wiki/%D0%A5%D0%B8%D0%B6%D0%B8%D0%BD%D0%B0_%D1%80%D1%8F%D0%B4%D0%BE%D0%BC_%D1%81_%D0%BC%D0%B0%D1%8F%D0%BA%D0%BE%D0%BC", + "Центральный Ярнам": "https://bloodborne.fandom.com/ru/wiki/%D0%A6%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%AF%D1%80%D0%BD%D0%B0%D0%BC", + "Церковь Доброй чаши": "https://bloodborne.fandom.com/ru/wiki/%D0%A6%D0%B5%D1%80%D0%BA%D0%BE%D0%B2%D1%8C_%D0%94%D0%BE%D0%B1%D1%80%D0%BE%D0%B9_%D1%87%D0%B0%D1%88%D0%B8", + "Церковь кошмара": "https://bloodborne.fandom.com/ru/wiki/%D0%A6%D0%B5%D1%80%D0%BA%D0%BE%D0%B2%D1%8C_%D0%BA%D0%BE%D1%88%D0%BC%D0%B0%D1%80%D0%B0", + "Часовня Идон": "https://bloodborne.fandom.com/ru/wiki/%D0%A7%D0%B0%D1%81%D0%BE%D0%B2%D0%BD%D1%8F_%D0%98%D0%B4%D0%BE%D0%BD", + "Яаар'гул, духовная деревня": "https://bloodborne.fandom.com/ru/wiki/%D0%AF%D0%B0%D0%B0%D1%80%27%D0%B3%D1%83%D0%BB,_%D0%B4%D1%83%D1%85%D0%BE%D0%B2%D0%BD%D0%B0%D1%8F_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BD%D1%8F" +} \ No newline at end of file diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_graph.html" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_graph.html" new file mode 100644 index 000000000..7a99fbe73 --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_graph.html" @@ -0,0 +1,145 @@ + + + + +Locations Bloodborne + + + + + + + + + \ No newline at end of file diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_transitions.json" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_transitions.json" new file mode 100644 index 000000000..95592ee00 --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/dumps/locations_transitions.json" @@ -0,0 +1,374 @@ +[ + [ + "Алтарь отчаяния", + "Сады светящихся цветов" + ], + [ + "Астральная часовая башня", + "Зал исследований" + ], + [ + "Астральная часовая башня", + "Рыбацкая деревня" + ], + [ + "Астральная часовая башня", + "Сад светящегося дерева" + ], + [ + "Большой мост", + "Центральный Ярнам" + ], + [ + "Бюргенверт", + "Запретный лес" + ], + [ + "Бюргенверт", + "Озеро лунного отражения" + ], + [ + "Верхний Соборный округ", + "Мастерская Церкви исцеления" + ], + [ + "Верхний Соборный округ", + "Соборный округ" + ], + [ + "Главный собор", + "Соборный округ" + ], + [ + "Главный собор кошмара", + "Кошмар охотника" + ], + [ + "Граница кошмара", + "Лекционный корпус" + ], + [ + "Гробница Идон", + "Центральный Ярнам" + ], + [ + "Гробница Идон", + "Часовня Идон" + ], + [ + "Зал Амигдалы", + "Граница кошмара" + ], + [ + "Зал исследований", + "Подземная куча трупов" + ], + [ + "Зал исследований", + "Сад светящегося дерева" + ], + [ + "Зал королевы Нечистокровных", + "Место Логариуса" + ], + [ + "Зал королевы Нечистокровных", + "Покинутый замок Кейнхёрст" + ], + [ + "Запретная могила", + "Бюргенверт" + ], + [ + "Запретная могила", + "Запретный лес" + ], + [ + "Запретный лес", + "Бюргенверт" + ], + [ + "Запретный лес", + "Лечебница Йозефки" + ], + [ + "Запретный лес", + "Соборный округ" + ], + [ + "Кладбище Черного чудовища", + "Подземная тюрьма" + ], + [ + "Кладбище Черного чудовища", + "Старый Ярнам" + ], + [ + "Кладбище Черного чудовища", + "Яаар'гул, духовная деревня" + ], + [ + "Кошмар Менсиса", + "Лекционный корпус" + ], + [ + "Кошмар Менсиса", + "Лунарий Кормилицы Мерго" + ], + [ + "Кошмар охотника", + "Подземная куча трупов" + ], + [ + "Кошмар охотника", + "Церковь кошмара" + ], + [ + "Лекционный корпус", + "Граница кошмара" + ], + [ + "Лекционный корпус", + "Кошмар Менсиса" + ], + [ + "Лекционный корпус", + "Площадь Пришествия" + ], + [ + "Лекционный корпус", + "Соборный округ" + ], + [ + "Лекционный корпус", + "Яаар'гул, духовная деревня" + ], + [ + "Лечебница Йозефки", + "Запретный лес" + ], + [ + "Лечебница Йозефки", + "Центральный Ярнам" + ], + [ + "Лунарий Кормилицы Мерго", + "Кошмар Менсиса" + ], + [ + "Мастерская Церкви исцеления", + "Верхний Соборный округ" + ], + [ + "Мастерская Церкви исцеления", + "Главный собор" + ], + [ + "Мастерская Церкви исцеления", + "Соборный округ" + ], + [ + "Мастерская Церкви исцеления", + "Старая заброшенная мастерская" + ], + [ + "Мастерская Церкви исцеления", + "Часовня Идон" + ], + [ + "Место Логариуса", + "Зал королевы Нечистокровных" + ], + [ + "Место Логариуса", + "Покинутый замок Кейнхёрст" + ], + [ + "Молельный переулок Хемвик", + "Обитель ведьмы" + ], + [ + "Молельный переулок Хемвик", + "Покинутый замок Кейнхёрст" + ], + [ + "Молельный переулок Хемвик", + "Соборный округ" + ], + [ + "Обитель ведьмы", + "Молельный переулок Хемвик" + ], + [ + "Озеро лунного отражения", + "Бюргенверт" + ], + [ + "Озеро лунного отражения", + "Яаар'гул, духовная деревня" + ], + [ + "Площадь Пришествия", + "Лекционный корпус" + ], + [ + "Площадь Пришествия", + "Яаар'гул, духовная деревня" + ], + [ + "Побережье", + "Рыбацкая деревня" + ], + [ + "Подземелье чаши", + "Сон охотника" + ], + [ + "Подземная куча трупов", + "Зал исследований" + ], + [ + "Подземная куча трупов", + "Кошмар охотника" + ], + [ + "Подземная куча трупов", + "Церковь кошмара" + ], + [ + "Подземная тюрьма", + "Старый Ярнам" + ], + [ + "Покинутый замок Кейнхёрст", + "Место Логариуса" + ], + [ + "Покинутый замок Кейнхёрст", + "Молельный переулок Хемвик" + ], + [ + "Рыбацкая деревня", + "Астральная часовая башня" + ], + [ + "Рыбацкая деревня", + "Побережье" + ], + [ + "Рыбацкая деревня", + "Хижина рядом с маяком" + ], + [ + "Сад светящегося дерева", + "Астральная часовая башня" + ], + [ + "Сад светящегося дерева", + "Зал исследований" + ], + [ + "Сады светящихся цветов", + "Алтарь отчаяния" + ], + [ + "Сады светящихся цветов", + "Верхний Соборный округ" + ], + [ + "Соборный округ", + "Запретный лес" + ], + [ + "Соборный округ", + "Кошмар охотника" + ], + [ + "Соборный округ", + "Лекционный корпус" + ], + [ + "Соборный округ", + "Мастерская Церкви исцеления" + ], + [ + "Соборный округ", + "Молельный переулок Хемвик" + ], + [ + "Соборный округ", + "Старый Ярнам" + ], + [ + "Соборный округ", + "Яаар'гул, духовная деревня" + ], + [ + "Старая заброшенная мастерская", + "Мастерская Церкви исцеления" + ], + [ + "Старый Ярнам", + "Соборный округ" + ], + [ + "Старый Ярнам", + "Яаар'гул, духовная деревня" + ], + [ + "Хижина рядом с маяком", + "Побережье" + ], + [ + "Хижина рядом с маяком", + "Рыбацкая деревня" + ], + [ + "Центральный Ярнам", + "Большой мост" + ], + [ + "Центральный Ярнам", + "Гробница Идон" + ], + [ + "Центральный Ярнам", + "Лечебница Йозефки" + ], + [ + "Церковь Доброй чаши", + "Старый Ярнам" + ], + [ + "Церковь кошмара", + "Кошмар охотника" + ], + [ + "Церковь кошмара", + "Подземная куча трупов" + ], + [ + "Часовня Идон", + "Мастерская Церкви исцеления" + ], + [ + "Часовня Идон", + "Соборный округ" + ], + [ + "Часовня Идон", + "Центральный Ярнам" + ], + [ + "Яаар'гул, духовная деревня", + "Лекционный корпус" + ], + [ + "Яаар'гул, духовная деревня", + "Озеро лунного отражения" + ], + [ + "Яаар'гул, духовная деревня", + "Площадь Пришествия" + ] +] \ No newline at end of file diff --git "a/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/main.py" "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/main.py" new file mode 100644 index 000000000..0bc856a10 --- /dev/null +++ "b/for_games/Bloodborne/bloodborne_fandom_com__ru__wiki__\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\233\320\276\320\272\320\260\321\206\320\270\320\270/main.py" @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import time +import sys + +from dataclasses import dataclass +from urllib.parse import urljoin +from pathlib import Path + +import requests +from bs4 import BeautifulSoup, Tag + + +DIR: Path = Path(__file__).resolve().parent + +sys.path.append( + str(DIR.parent.parent / "Dark Souls" / "generate_HTML_location_graph__with_d3js") +) +from generate_graph_html import generate as generate_graph_html + + +DIR_DUMPS: Path = DIR / "dumps" +DIR_DUMPS.mkdir(parents=True, exist_ok=True) + +URL: str = "https://bloodborne.fandom.com/ru/wiki/Категория:Локации" + + +@dataclass +class Location: + title: str + url: str + + @classmethod + def from_tag(cls, tag: Tag) -> "Location": + return cls( + title=tag["title"], + url=urljoin(URL, tag["href"]), + ) + + +links: set[tuple[str, str]] = set() +location_by_url: dict[str, str] = dict() + +rs = requests.get(URL) +rs.raise_for_status() + +soup = BeautifulSoup(rs.content, "html.parser") +locations: list[Location] = [ + Location.from_tag(a) + for a in soup.select("a.category-page__member-link[href][title]") +] + +for location in locations: + time.sleep(1) + for _ in range(3): + try: + # TODO: Оптимизации не хватает + rs = requests.get(location.url) + rs.raise_for_status() + except Exception as e: + print(f"Error: {e}, url: {location.url}") + time.sleep(1) + + soup = BeautifulSoup(rs.content, "html.parser") + + to_locations: list[tuple[str, str]] = [ + (a["title"], urljoin(rs.url, a["href"])) + for a in soup.select('[data-source="Переходы"] a[href][title]') + ] + # У любой локации есть связанная с ней локация. Если нет - значит это страница не про локацию + if not to_locations: + continue + + location_by_url[location.title] = location.url + + print(location.title, location.url) + print(" Переходы:") + + # Найдем связанные локации + for to_title, to_url in to_locations: + # Проверяем что ссылка ведет на локацию + if not any(True for l in locations if l.url == to_url): # TODO: Рефакторинг? + continue + + print(f" {to_title}: {to_url}") + location_by_url[to_title] = to_url + + links.add((location.title, to_title)) + +print() + +# Экспорт в json +location_by_url: dict[str, str] = dict(sorted(location_by_url.items())) +json.dump( + location_by_url, + open(DIR_DUMPS / "locations.json", "w", encoding="utf-8"), + ensure_ascii=False, + indent=4, +) + +links: list[tuple[str, str]] = sorted(links) +json.dump( + links, + open(DIR_DUMPS / "locations_transitions.json", "w", encoding="utf-8"), + ensure_ascii=False, + indent=4, +) + +generate_graph_html( + file_name_to=DIR_DUMPS / "locations_graph.html", + links=links, + title="Locations Bloodborne", +) diff --git a/for_games/Bot Buff Knight Advanced/__foo_test.py b/for_games/Bot Buff Knight Advanced/__foo_test.py index 05cfafd8d..f4ed97917 100644 --- a/for_games/Bot Buff Knight Advanced/__foo_test.py +++ b/for_games/Bot Buff Knight Advanced/__foo_test.py @@ -20,7 +20,7 @@ from common import get_current_datetime_str -def draw_rects(img, contours_rects, color=(0, 255, 0)): +def draw_rects(img, contours_rects, color=(0, 255, 0)) -> None: 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) diff --git a/for_games/Bot Buff Knight Advanced/common.py b/for_games/Bot Buff Knight Advanced/common.py index 4dfbf8bcc..0ebae9a97 100644 --- a/for_games/Bot Buff Knight Advanced/common.py +++ b/for_games/Bot Buff Knight Advanced/common.py @@ -24,7 +24,7 @@ def get_logger(name, file="log.txt", encoding="utf-8", log_stdout=True, log_file if log_file: fh = RotatingFileHandler( - file, maxBytes=10000000, backupCount=5, encoding=encoding + file, maxBytes=10_000_000, backupCount=5, encoding=encoding ) fh.setFormatter(formatter) log.addHandler(fh) diff --git a/for_games/Bot Buff Knight Advanced/main.py b/for_games/Bot Buff Knight Advanced/main.py index 78770abe8..26105e37c 100644 --- a/for_games/Bot Buff Knight Advanced/main.py +++ b/for_games/Bot Buff Knight Advanced/main.py @@ -61,7 +61,7 @@ def filter_fairy_and_button(rect_fairy, rect_button): os.mkdir(DIR) -def save_screenshot(prefix, img_hsv): +def save_screenshot(prefix, img_hsv) -> None: file_name = f"{DIR}/{prefix}__{get_current_datetime_str()}.png" log.debug(file_name) cv2.imwrite(file_name, cv2.cvtColor(np.array(img_hsv), cv2.COLOR_HSV2BGR)) @@ -90,12 +90,12 @@ def save_screenshot(prefix, img_hsv): } -def change_start(): +def change_start() -> None: BOT_DATA["START"] = not BOT_DATA["START"] log.debug("START: %s", BOT_DATA["START"]) -def change_auto_attack(): +def change_auto_attack() -> None: BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] log.debug("AUTO_ATTACK: %s", BOT_DATA["AUTO_ATTACK"]) @@ -105,7 +105,7 @@ def change_auto_attack(): log.debug('Press "%s" for AUTO_ATTACK', AUTO_ATTACK_COMBINATION) -def process_auto_attack(): +def process_auto_attack() -> None: while True: if not BOT_DATA["START"]: time.sleep(0.01) @@ -118,7 +118,7 @@ def process_auto_attack(): time.sleep(0.01) -def process_find_fairy(img_hsv): +def process_find_fairy(img_hsv) -> None: 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) diff --git a/for_games/Dark Souls/backup__save_files/by_autosave/common.py b/for_games/Dark Souls/backup__save_files/by_autosave/common.py index cfaa2eced..d9a398a5a 100644 --- a/for_games/Dark Souls/backup__save_files/by_autosave/common.py +++ b/for_games/Dark Souls/backup__save_files/by_autosave/common.py @@ -6,11 +6,13 @@ import os import time -from glob import glob import traceback +from glob import glob + # For import utils.py import sys + sys.path.append("..") from utils import get_logger, backup @@ -19,26 +21,28 @@ log = get_logger(__file__) -def backup_saves(path_ds_save: str, forced=False): +def backup_saves(path_ds_save: str, forced: bool = False, modified_minutes: int = 5) -> None: 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_timestamp: float = os.path.getmtime(path_file_name) + now_timestamp: float = 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 + # Save backup. If less than modified_minutes have passed since the last modification of the file + is_modified: bool = (now_timestamp - save_timestamp) < ( + modified_minutes * 60 + ) + ok: bool = 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}" + f"Reason: Forced={forced}, is modified file save={is_modified}" ) if not ok: continue - file_name_backup = backup(path_file_name, now_timestamp) + file_name_backup: str = backup(path_file_name, now_timestamp) log.debug(f"Saving backup: {file_name_backup}") except: @@ -46,9 +50,9 @@ def backup_saves(path_ds_save: str, forced=False): time.sleep(5 * 60) -def run(path_ds_save: str, timeout_minutes=5): +def run(path_ds_save: str, timeout_minutes: int = 5) -> None: # Example: r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' - path_ds_save = os.path.expanduser(path_ds_save) + path_ds_save: str = os.path.expanduser(path_ds_save) print(f"{path_ds_save}\n") @@ -58,4 +62,4 @@ def run(path_ds_save: str, timeout_minutes=5): print(f"\nSleeping for {timeout_minutes} minutes\n") time.sleep(timeout_minutes * 60) - backup_saves(path_ds_save, False) + backup_saves(path_ds_save, forced=False, modified_minutes=timeout_minutes) diff --git a/for_games/Dark Souls/backup__save_files/by_autosave/ds1.py b/for_games/Dark Souls/backup__save_files/by_autosave/ds1.py index e7f0f86f9..3c11fadad 100644 --- a/for_games/Dark Souls/backup__save_files/by_autosave/ds1.py +++ b/for_games/Dark Souls/backup__save_files/by_autosave/ds1.py @@ -8,6 +8,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py b/for_games/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py index b8b1b8cc4..4e484355a 100644 --- a/for_games/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py +++ b/for_games/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py @@ -8,6 +8,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_autosave/ds2.py b/for_games/Dark Souls/backup__save_files/by_autosave/ds2.py index 954da1bd6..5bcf92e3a 100644 --- a/for_games/Dark Souls/backup__save_files/by_autosave/ds2.py +++ b/for_games/Dark Souls/backup__save_files/by_autosave/ds2.py @@ -7,6 +7,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_autosave/ds3.py b/for_games/Dark Souls/backup__save_files/by_autosave/ds3.py index 7c62dfe91..13523a69e 100644 --- a/for_games/Dark Souls/backup__save_files/by_autosave/ds3.py +++ b/for_games/Dark Souls/backup__save_files/by_autosave/ds3.py @@ -7,6 +7,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_hotkey/common.py b/for_games/Dark Souls/backup__save_files/by_hotkey/common.py index b7a1de068..345c3c0c4 100644 --- a/for_games/Dark Souls/backup__save_files/by_hotkey/common.py +++ b/for_games/Dark Souls/backup__save_files/by_hotkey/common.py @@ -21,7 +21,7 @@ import keyboard -def beep(): +def beep() -> None: try: winsound.Beep(1000, duration=50) except: @@ -35,7 +35,7 @@ def beep(): HOTKEY = "CTRL + F1" -def backup_saves(path_ds_save: str): +def backup_saves(path_ds_save: str) -> None: try: for path_file_name in glob(path_ds_save): file_name_backup = backup(path_file_name) @@ -48,7 +48,7 @@ def backup_saves(path_ds_save: str): time.sleep(5 * 60) -def run(path_ds_save: str, hotkey=HOTKEY): +def run(path_ds_save: str, hotkey: str = HOTKEY) -> None: # Example: r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' path_ds_save = os.path.expanduser(path_ds_save) diff --git a/for_games/Dark Souls/backup__save_files/by_hotkey/ds1.py b/for_games/Dark Souls/backup__save_files/by_hotkey/ds1.py index 8eae6535b..f5c6c206f 100644 --- a/for_games/Dark Souls/backup__save_files/by_hotkey/ds1.py +++ b/for_games/Dark Souls/backup__save_files/by_hotkey/ds1.py @@ -8,6 +8,7 @@ # 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: diff --git a/for_games/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py b/for_games/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py index b8b1b8cc4..4e484355a 100644 --- a/for_games/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py +++ b/for_games/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py @@ -8,6 +8,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_hotkey/ds2.py b/for_games/Dark Souls/backup__save_files/by_hotkey/ds2.py index 954da1bd6..5bcf92e3a 100644 --- a/for_games/Dark Souls/backup__save_files/by_hotkey/ds2.py +++ b/for_games/Dark Souls/backup__save_files/by_hotkey/ds2.py @@ -7,6 +7,7 @@ # 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/for_games/Dark Souls/backup__save_files/by_hotkey/ds3.py b/for_games/Dark Souls/backup__save_files/by_hotkey/ds3.py index 7c62dfe91..13523a69e 100644 --- a/for_games/Dark Souls/backup__save_files/by_hotkey/ds3.py +++ b/for_games/Dark Souls/backup__save_files/by_hotkey/ds3.py @@ -7,6 +7,7 @@ # 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/for_games/Dark Souls/backup__save_files/utils.py b/for_games/Dark Souls/backup__save_files/utils.py index ff8205783..3ae76339f 100644 --- a/for_games/Dark Souls/backup__save_files/utils.py +++ b/for_games/Dark Souls/backup__save_files/utils.py @@ -11,7 +11,7 @@ import sys -def get_logger(name): +def get_logger(name: str) -> logging.Logger: log = logging.getLogger(name) log.setLevel(logging.DEBUG) @@ -24,7 +24,7 @@ def get_logger(name): return log -def backup(path_file_name: str, now_timestamp=None) -> str: +def backup(path_file_name: str, now_timestamp: float | None = None) -> str: if now_timestamp is None: now_timestamp = time.time() diff --git a/for_games/Dark Souls/common.py b/for_games/Dark Souls/common.py index 5ecfb72cf..ae3d620bf 100644 --- a/for_games/Dark Souls/common.py +++ b/for_games/Dark Souls/common.py @@ -17,7 +17,7 @@ class Parser: - def __init__(self, url_locations: str, log=True): + def __init__(self, url_locations: str, log=True) -> None: self._url_locations = url_locations self._is_log = log @@ -43,7 +43,7 @@ def DS2(log=True) -> "Parser": def DS3(log=True) -> "Parser": return Parser(URL_DS3, log) - def _log(self, *args, **kwargs): + def _log(self, *args, **kwargs) -> None: self._is_log and print(*args, **kwargs) @staticmethod diff --git a/for_games/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py b/for_games/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py index ce3f9bbd7..edeadb617 100644 --- a/for_games/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py +++ b/for_games/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py @@ -4,6 +4,12 @@ __author__ = "ipetrash" +from pathlib import Path + + +DIR: Path = Path(__file__).resolve().parent + + def get_dataset_text(links: list[tuple[str, str]]) -> str: # Список всех узлов графа nodes = set() @@ -54,16 +60,17 @@ def get_dataset_text(links: list[tuple[str, str]]) -> str: def generate( - file_name_to: str, + file_name_to: str | Path, links: list[tuple[str, str]], title: str = "Graph", -): + file_name_template: str | Path = DIR / "template_graph.html", +) -> None: template_title = "{{title}}" template_dataset = "{{dataset}}" dataset_text = get_dataset_text(links) - with open("template_graph.html", "r", encoding="utf-8") as f: + with open(file_name_template, "r", encoding="utf-8") as f: text = f.read() text = text.replace(template_title, title) text = text.replace(template_dataset, dataset_text) diff --git a/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py b/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py index a6ed481dd..f5bd70785 100644 --- a/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py +++ b/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py @@ -228,7 +228,7 @@ def print_stats( rows: List[Tuple[str, int, int]], headers=("NAME", "HEALTH", "SOULS"), sort_column=1, -): +) -> None: rows.sort(key=lambda x: x[sort_column], reverse=True) rows.insert(0, headers) diff --git a/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py b/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py index 4b897e8e2..c0d5129c4 100644 --- a/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py +++ b/for_games/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py @@ -85,7 +85,7 @@ def get_bosses(url: str) -> Dict[str, List[Boss]]: return bosses_by_category -def print_bosses(url: str, bosses: Dict[str, List[Boss]]): +def print_bosses(url: str, bosses: Dict[str, List[Boss]]) -> None: total = sum(len(i) for i in bosses.values()) print(f"{url} ({total}):") @@ -108,7 +108,7 @@ def convert_bosses_to_only_name(bosses: Dict[str, List[Boss]]) -> Dict[str, List return bosses_only_name -def export_to_json(file_name, bosses): +def export_to_json(file_name, bosses) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) @@ -120,7 +120,7 @@ def export_to_json(file_name, bosses): ) -def export_to_sqlite(file_name: str, bosses_ds123: Dict[str, Dict[str, List[Boss]]]): +def export_to_sqlite(file_name: str, bosses_ds123: Dict[str, Dict[str, List[Boss]]]) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) diff --git "a/for_games/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" "b/for_games/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" index aba252d9f..aecf16556 100644 --- "a/for_games/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" +++ "b/for_games/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" @@ -55,7 +55,7 @@ def get_bosses(url: str) -> dict[str, list[Boss]]: return bosses_by_category -def print_bosses(url: str, bosses: dict[str, list[Boss]]): +def print_bosses(url: str, bosses: dict[str, list[Boss]]) -> None: total = sum(len(i) for i in bosses.values()) print(f"{url} ({total}):") @@ -78,7 +78,7 @@ def convert_bosses_to_only_name(bosses: dict[str, list[Boss]]) -> dict[str, list return bosses_only_name -def export_to_json(file_name, bosses): +def export_to_json(file_name, bosses) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) @@ -90,7 +90,7 @@ def export_to_json(file_name, bosses): ) -def export_to_simple_text(file_name, bosses): +def export_to_simple_text(file_name, bosses) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) diff --git a/for_games/Final_Fantasy_II__upgrade_skills_bot.py b/for_games/Final_Fantasy_II__upgrade_skills_bot.py index 7bdf830f7..d28f9f1a8 100644 --- a/for_games/Final_Fantasy_II__upgrade_skills_bot.py +++ b/for_games/Final_Fantasy_II__upgrade_skills_bot.py @@ -31,7 +31,7 @@ def get_logger(name, file="log.txt", encoding="utf-8", log_stdout=True, log_file if log_file: fh = RotatingFileHandler( - file, maxBytes=10000000, backupCount=5, encoding=encoding + file, maxBytes=10_000_000, backupCount=5, encoding=encoding ) fh.setFormatter(formatter) log.addHandler(fh) @@ -44,7 +44,7 @@ def get_logger(name, file="log.txt", encoding="utf-8", log_stdout=True, log_file return log -def change_start(): +def change_start() -> None: DATA["START"] = not DATA["START"] if DATA["START"]: DATA["COUNTER"] = 0 @@ -52,17 +52,17 @@ def change_start(): log.debug("Change START: %s", DATA["START"]) -def set_min_counter(): +def set_min_counter() -> None: DATA["MAX_COUNTER"] = 1 log.debug("Change MAX_COUNTER: %s", DATA["MAX_COUNTER"]) -def set_max_counter(): +def set_max_counter() -> None: DATA["MAX_COUNTER"] = MAX_COUNTER log.debug("Change MAX_COUNTER: %s", DATA["MAX_COUNTER"]) -def change_cancel_all_last(): +def change_cancel_all_last() -> None: DATA["CANCEL_ALL_LAST"] = not DATA["CANCEL_ALL_LAST"] log.debug("Change CANCEL_ALL_LAST: %s", DATA["CANCEL_ALL_LAST"]) diff --git a/for_games/Hollow_Knight/export_to_json.py b/for_games/Hollow_Knight/export_to_json.py index 2a5100126..79d6c03c9 100644 --- a/for_games/Hollow_Knight/export_to_json.py +++ b/for_games/Hollow_Knight/export_to_json.py @@ -14,7 +14,7 @@ def export_to_json_str(bosses: dict[str, list]) -> str: return json.dumps(bosses, ensure_ascii=False, indent=4) -def export_to_json(file_name: str, bosses: dict[str, list]): +def export_to_json(file_name: str, bosses: dict[str, list]) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) diff --git a/for_games/Hollow_Knight/print_bosses.py b/for_games/Hollow_Knight/print_bosses.py index bc1ded54a..68fb34975 100644 --- a/for_games/Hollow_Knight/print_bosses.py +++ b/for_games/Hollow_Knight/print_bosses.py @@ -7,7 +7,7 @@ from utils import get_bosses, Boss -def print_bosses(bosses: dict[str, list[Boss]], only_names=False): +def print_bosses(bosses: dict[str, list[Boss]], only_names=False) -> None: total = sum(len(i) for i in bosses.values()) print(f"Hollow Knight ({total}):") diff --git a/for_games/RMMZ_update_fields_CGMZ_GameInfo.py b/for_games/RMMZ_update_fields_CGMZ_GameInfo.py new file mode 100644 index 000000000..f48025af3 --- /dev/null +++ b/for_games/RMMZ_update_fields_CGMZ_GameInfo.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import subprocess + +import re + +from pathlib import Path + + +PATTERN_FIELD_VERSION: re.Pattern = re.compile(r'(?P"Left Text"\s*:\s*)".+?"') + + +def process(path_dir: Path, forced: bool = False) -> None: + path_js_plugins: Path = path_dir / "js" / "plugins.js" + + # If the last commit was in path_js_plugins, then skipping the file change + result: bytes = subprocess.check_output( + args=["git", "diff", "HEAD~1", "HEAD", path_js_plugins], + cwd=path_dir, + ) + if result and not forced: + print( + f"Skipping change {path_js_plugins} because the file is in the last commit" + ) + return + + result: str = subprocess.check_output( + args=[ + "git", + "log", + "-1", + '--pretty=format:{"hash":"%h","datetime":"%ad"}', + "--date=format:%Y-%m-%d %H:%M:%S", + ], + cwd=path_dir, + encoding="utf-8", + ) + print("Git log:", result) + + data: dict[str, str] = json.loads(result) + print("Git log (parsed):", data) + + version: str = f'v{data["hash"]} ({data["datetime"]})' + print(f"Version: {version}") + + js_plugins: str = path_js_plugins.read_text(encoding="utf-8") + lines: list[str] = js_plugins.splitlines() + for i, line in enumerate(lines): + if "CGMZ_GameInfo" not in line: + continue + + print(f"Found line {line!r}") + + m = PATTERN_FIELD_VERSION.search(line) + if not m: + continue + + current_field_version: str = m.group() + + new_field_version: str = PATTERN_FIELD_VERSION.sub( + rf'\g"{version}"', current_field_version + ) + print(f"Replace: {current_field_version!r} -> {new_field_version!r}") + + lines[i] = line.replace(current_field_version, new_field_version) + + print(f"Write to {path_js_plugins}") + path_js_plugins.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Writes information about the last git commit to the CGMZ_GameInfo plugin parameter." + ) + parser.add_argument( + "path_project", + help="Path to project (the directory containing game.rmmzproject)", + type=Path, + ) + parser.add_argument( + "--forced", + help="Force update", + action="store_true", + default=False, + ) + args = parser.parse_args() + + process( + path_dir=args.path_project, + forced=args.forced, + ) diff --git a/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py b/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py index 2c9f8e7d1..2f81f91e2 100644 --- a/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py +++ b/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py @@ -14,7 +14,7 @@ def export_to_json_str(bosses: dict[str, list[str]]) -> str: return json.dumps(bosses, ensure_ascii=False, indent=4) -def export_to_json(file_name: str, bosses: dict[str, list[str]]): +def export_to_json(file_name: str, bosses: dict[str, list[str]]) -> None: dir_name = os.path.dirname(file_name) os.makedirs(dir_name, exist_ok=True) diff --git a/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py b/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py index b190b08b5..7ce020059 100644 --- a/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py +++ b/for_games/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py @@ -7,7 +7,7 @@ from utils import get_bosses, Boss -def print_bosses(bosses: dict[str, list[Boss]], only_names=False): +def print_bosses(bosses: dict[str, list[Boss]], only_names=False) -> None: total = sum(len(i) for i in bosses.values()) print(f"Salt and Sanctuary ({total}):") diff --git a/html_parsing/denuvo__get_games/game_debate_com.py b/for_games/denuvo__get_games/game_debate_com.py similarity index 100% rename from html_parsing/denuvo__get_games/game_debate_com.py rename to for_games/denuvo__get_games/game_debate_com.py diff --git a/html_parsing/denuvo__get_games/gamicus_fandom_com__wiki__Denuvo.py b/for_games/denuvo__get_games/gamicus_fandom_com__wiki__Denuvo.py similarity index 100% rename from html_parsing/denuvo__get_games/gamicus_fandom_com__wiki__Denuvo.py rename to for_games/denuvo__get_games/gamicus_fandom_com__wiki__Denuvo.py diff --git a/html_parsing/denuvo__get_games/store_steampowered_com__curator__26095454_Denuvo_Games__list__38826.py b/for_games/denuvo__get_games/store_steampowered_com__curator__26095454_Denuvo_Games__list__38826.py similarity index 100% rename from html_parsing/denuvo__get_games/store_steampowered_com__curator__26095454_Denuvo_Games__list__38826.py rename to for_games/denuvo__get_games/store_steampowered_com__curator__26095454_Denuvo_Games__list__38826.py diff --git a/html_parsing/exclusive_games/common.py b/for_games/exclusive_games/common.py similarity index 100% rename from html_parsing/exclusive_games/common.py rename to for_games/exclusive_games/common.py diff --git a/html_parsing/exclusive_games/ps3__gematsu_com.py b/for_games/exclusive_games/ps3__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/ps3__gematsu_com.py rename to for_games/exclusive_games/ps3__gematsu_com.py diff --git a/html_parsing/exclusive_games/ps4__gematsu_com.py b/for_games/exclusive_games/ps4__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/ps4__gematsu_com.py rename to for_games/exclusive_games/ps4__gematsu_com.py diff --git a/html_parsing/exclusive_games/ps4__ru_wikipedia_org.py b/for_games/exclusive_games/ps4__ru_wikipedia_org.py similarity index 100% rename from html_parsing/exclusive_games/ps4__ru_wikipedia_org.py rename to for_games/exclusive_games/ps4__ru_wikipedia_org.py diff --git a/html_parsing/exclusive_games/psp__listal_com.py b/for_games/exclusive_games/psp__listal_com.py similarity index 100% rename from html_parsing/exclusive_games/psp__listal_com.py rename to for_games/exclusive_games/psp__listal_com.py diff --git a/html_parsing/exclusive_games/switch__gematsu_com.py b/for_games/exclusive_games/switch__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/switch__gematsu_com.py rename to for_games/exclusive_games/switch__gematsu_com.py diff --git a/html_parsing/exclusive_games/wii_u__gematsu_com.py b/for_games/exclusive_games/wii_u__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/wii_u__gematsu_com.py rename to for_games/exclusive_games/wii_u__gematsu_com.py diff --git a/html_parsing/exclusive_games/xbox_360__gematsu_com.py b/for_games/exclusive_games/xbox_360__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/xbox_360__gematsu_com.py rename to for_games/exclusive_games/xbox_360__gematsu_com.py diff --git a/html_parsing/exclusive_games/xbox_360__ru_wikipedia_org.py b/for_games/exclusive_games/xbox_360__ru_wikipedia_org.py similarity index 100% rename from html_parsing/exclusive_games/xbox_360__ru_wikipedia_org.py rename to for_games/exclusive_games/xbox_360__ru_wikipedia_org.py diff --git a/html_parsing/exclusive_games/xbox_one__gematsu_com.py b/for_games/exclusive_games/xbox_one__gematsu_com.py similarity index 100% rename from html_parsing/exclusive_games/xbox_one__gematsu_com.py rename to for_games/exclusive_games/xbox_one__gematsu_com.py diff --git a/html_parsing/exclusive_games/xbox_one__ru_wikipedia_org.py b/for_games/exclusive_games/xbox_one__ru_wikipedia_org.py similarity index 100% rename from html_parsing/exclusive_games/xbox_one__ru_wikipedia_org.py rename to for_games/exclusive_games/xbox_one__ru_wikipedia_org.py diff --git a/for_games/fitgirl_RE_Village_with_win10_notify.py b/for_games/fitgirl_RE_Village_with_win10_notify.py index 99ce1dd7b..e0b5e42f8 100644 --- a/for_games/fitgirl_RE_Village_with_win10_notify.py +++ b/for_games/fitgirl_RE_Village_with_win10_notify.py @@ -25,7 +25,7 @@ from run_notify import run_in_thread -def show_notify(name: str): +def show_notify(name: str) -> None: title = f"Уведомление за {datetime.now():%Y/%m/%d %H:%M:%S}" print(f"{title}\n{name}\n") diff --git a/game__get_random_rarity_item.py b/for_games/game__get_random_rarity_item.py similarity index 92% rename from game__get_random_rarity_item.py rename to for_games/game__get_random_rarity_item.py index 6808f27be..b0afc828b 100644 --- a/game__get_random_rarity_item.py +++ b/for_games/game__get_random_rarity_item.py @@ -7,7 +7,7 @@ import random -def get_random_rarity_item(): +def get_random_rarity_item() -> str: a = random.randrange(1, 1000) if a <= 10: diff --git a/for_games/gamestatus_info__lastcrackedgames.py b/for_games/gamestatus_info__lastcrackedgames.py index 961bc246d..0c5e7ca26 100644 --- a/for_games/gamestatus_info__lastcrackedgames.py +++ b/for_games/gamestatus_info__lastcrackedgames.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from datetime import date +from typing import Any import requests @@ -26,20 +27,23 @@ class Game: title: str url: str protection: str + hacked_groups: str release_date: date crack_date: date @classmethod - def parse_from(cls, data: dict) -> "Game": - # Example: "[\"DENUVO\"]" -> "DENUVO", "denuvo" -> "DENUVO" - protection: str = data["protections"].upper() - if protection.startswith("["): - protection = ", ".join(json.loads(protection)) + def from_dict(cls, data: dict[str, Any]) -> "Game": + def _get_title(key: str) -> str: + title: str = data[key] + if title.startswith("["): + title = ", ".join(json.loads(title)) + return title return cls( title=data["title"], url=f'{URL_BASE}/{data["slug"]}', - protection=protection, + protection=_get_title("protections"), + hacked_groups=_get_title("hacked_groups"), release_date=date.fromisoformat(data["release_date"]), crack_date=date.fromisoformat(data["crack_date"]), ) @@ -49,7 +53,7 @@ def get_games() -> list[Game]: rs = session.get(f"{URL_BASE}/back/api/gameinfo/game/lastcrackedgames/") rs.raise_for_status() - return [Game.parse_from(game) for game in rs.json()["list_crack_games"]] + return [Game.from_dict(game) for game in rs.json()["list_crack_games"]] if __name__ == "__main__": @@ -59,11 +63,13 @@ def get_games() -> list[Game]: print(f"{i}. {game}") """ Games (200): - 1. Game(title='High On Life', url='https://gamestatus.info/high-on-life', protection='STEAM', release_date=datetime.date(2022, 12, 13), crack_date=datetime.date(2022, 12, 13)) - 2. Game(title='CRISIS CORE –FINAL FANTASY VII– REUNION', url='https://gamestatus.info/crisis-core-final-fantasy-vii-reunion', protection='STEAM', release_date=datetime.date(2022, 12, 13), crack_date=datetime.date(2022, 12, 13)) - 3. Game(title='Wavetale', url='https://gamestatus.info/wavetale', protection='STEAM', release_date=datetime.date(2022, 12, 12), crack_date=datetime.date(2022, 12, 12)) + 1. Game(title='RAIDOU Remastered: The Mystery of the Soulless Army', url='https://gamestatus.info/raidou-remastered-the-mystery-of-the-soulless-army', protection='DRM', hacked_groups='DenuvOwO Hypervisor — это обход защиты, при котором обычно необходимо отключить защиту ПК, что делает ваше устройство более уязвимым. Будьте осторожны!', release_date=datetime.date(2025, 6, 18), crack_date=datetime.date(2026, 3, 31)) + 2. Game(title="Sid Meier's Civilization® VII - Founders Edition", url='https://gamestatus.info/sid-meiers-civilization-vii', protection='Denuvo Anti-tamper', hacked_groups='DenuvOwO Hypervisor — это обход защиты, при котором обычно необходимо отключить защиту ПК, что делает ваше устройство более уязвимым. Будьте осторожны!', release_date=datetime.date(2025, 2, 10), crack_date=datetime.date(2026, 3, 31)) + 3. Game(title='Persona 4 Arena Ultimax', url='https://gamestatus.info/persona-4-arena-ultimax', protection='DENUVO', hacked_groups='DenuvOwO Hypervisor — это обход защиты, при котором обычно необходимо отключить защиту ПК, что делает ваше устройство более уязвимым. Будьте осторожны!', release_date=datetime.date(2022, 3, 16), crack_date=datetime.date(2026, 3, 30)) + 4. Game(title='SUPER ROBOT WARS Y', url='https://gamestatus.info/super-robot-wars-y', protection='Denuvo Anti-Tamper', hacked_groups='DenuvOwO Hypervisor — это обход защиты, при котором обычно необходимо отключить защиту ПК, что делает ваше устройство более уязвимым. Будьте осторожны!', release_date=datetime.date(2025, 8, 27), crack_date=datetime.date(2026, 3, 30)) + 5. Game(title='Hozy', url='https://gamestatus.info/hozy', protection='STEAM', hacked_groups='RUNE', release_date=datetime.date(2026, 3, 30), crack_date=datetime.date(2026, 3, 30)) ... - 198. Game(title='Spire of Sorcery', url='https://gamestatus.info/spire-of-sorcery', protection='STEAM', release_date=datetime.date(2021, 10, 21), crack_date=datetime.date(2021, 10, 21)) - 199. Game(title='Inscryption', url='https://gamestatus.info/Inscryption', protection='STEAM', release_date=datetime.date(2021, 10, 19), crack_date=datetime.date(2021, 10, 19)) - 200. Game(title='Youtubers Life 2', url='https://gamestatus.info/youtubers-life-2', protection='STEAM', release_date=datetime.date(2021, 10, 19), crack_date=datetime.date(2021, 10, 19)) + 198. Game(title='Under The Island', url='https://gamestatus.info/under-the-island', protection='GOG', hacked_groups='DRM-Free', release_date=datetime.date(2026, 2, 17), crack_date=datetime.date(2026, 2, 17)) + 199. Game(title='Fisherman Simulator', url='https://gamestatus.info/fisherman-simulator', protection='STEAM', hacked_groups='Tenoke', release_date=datetime.date(2026, 2, 13), crack_date=datetime.date(2026, 2, 17)) + 200. Game(title='Styx: Blades of Greed', url='https://gamestatus.info/styx-blades-of-greed', protection='STEAM', hacked_groups='RUNE', release_date=datetime.date(2026, 2, 19), crack_date=datetime.date(2026, 2, 17)) """ diff --git a/for_games/gamesvoice_ru__get_finished.py b/for_games/gamesvoice_ru__get_finished.py new file mode 100644 index 000000000..013095374 --- /dev/null +++ b/for_games/gamesvoice_ru__get_finished.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from datetime import datetime, date +from typing import Any + +import requests + + +@dataclass +class Game: + title_eng: str + title_rus: str | None + url: str + published_at: date + + +session = requests.session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0" + + +def get_games() -> list[Game]: + rs = session.get( + "https://api.gamesvoice.ru/api/products/all", + params=dict( + is_develop=0, + not_by_published=1, # По дате выхода (новые-старые) + ), + ) + rs.raise_for_status() + + result: dict[str, Any] = rs.json() + assert result.get("success"), "Получен неуспешный ответ" + + # NOTE Пример obj: + """ + { + "id": 7, + "title_eng": "Alan Wake II", + "title_rus": null, + "preview": "https://api.gamesvoice.ru/storage/uploads/products/previews/37c4e26ba5c1b074367b6a8d2c53ad3e.png", + "search_preview": "https://api.gamesvoice.ru/storage/uploads/products/search_previews/0f590f1f3951cdd1c6271c8af62ef431.png", + "search_description": "\u003Cp\u003EAlan Wake II — это игра в жанре survival-horror от третьего лица. Игрокам предсто", + "alias": "alanwake2", + "published_at": "2025-04-17 00:00:00", + "renewal_at": "2025-05-05 00:00:00" + } + """ + return [ + Game( + title_eng=obj["title_eng"], + title_rus=obj["title_rus"], + url=f"https://www.gamesvoice.ru/product/{obj['alias']}", + published_at=datetime.fromisoformat(obj["published_at"]).date(), + ) + for obj in result["data"] + ] + + +if __name__ == "__main__": + items = get_games() + print(f"Games ({len(items)}):") + for i, game in enumerate(items, 1): + print(f" {i}. {game}") + """ + Games (71): + 1. Game(title_eng='Volkolak: The Will of Gods', title_rus='Волколак: Воля Богов', url='https://www.gamesvoice.ru/product/volkolak', published_at=datetime.date(2025, 8, 22)) + 2. Game(title_eng='One-Eyed Likho', title_rus='ЛИХО ОДНОГЛАЗОЕ', url='https://www.gamesvoice.ru/product/likho', published_at=datetime.date(2025, 7, 28)) + 3. Game(title_eng='Silent Hill 2', title_rus='Сайлент Хилл 2', url='https://www.gamesvoice.ru/product/silenthill2', published_at=datetime.date(2025, 6, 28)) + ... + 69. Game(title_eng='Panzer Corps', title_rus=None, url='https://www.gamesvoice.ru/product/panzercorps', published_at=datetime.date(2012, 7, 8)) + 70. Game(title_eng='Mad Riders', title_rus=None, url='https://www.gamesvoice.ru/product/madriders', published_at=datetime.date(2012, 7, 3)) + 71. Game(title_eng='Defenders of Ardania', title_rus=None, url='https://www.gamesvoice.ru/product/doa', published_at=datetime.date(2012, 5, 26)) + """ diff --git a/html_parsing/gametime__use_cubiq_ru/extract_all_games.json b/for_games/gametime__use_cubiq_ru/extract_all_games.json similarity index 100% rename from html_parsing/gametime__use_cubiq_ru/extract_all_games.json rename to for_games/gametime__use_cubiq_ru/extract_all_games.json diff --git a/html_parsing/gametime__use_cubiq_ru/extract_all_games.py b/for_games/gametime__use_cubiq_ru/extract_all_games.py similarity index 100% rename from html_parsing/gametime__use_cubiq_ru/extract_all_games.py rename to for_games/gametime__use_cubiq_ru/extract_all_games.py diff --git a/html_parsing/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py b/for_games/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py similarity index 100% rename from html_parsing/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py rename to for_games/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py diff --git a/html_parsing/gametime__use_cubiq_ru/found_from_local_files.py b/for_games/gametime__use_cubiq_ru/found_from_local_files.py similarity index 100% rename from html_parsing/gametime__use_cubiq_ru/found_from_local_files.py rename to for_games/gametime__use_cubiq_ru/found_from_local_files.py diff --git a/html_parsing/gametime__use_cubiq_ru/main.py b/for_games/gametime__use_cubiq_ru/main.py similarity index 100% rename from html_parsing/gametime__use_cubiq_ru/main.py rename to for_games/gametime__use_cubiq_ru/main.py diff --git a/for_games/grid_clicker/grid_clicker.py b/for_games/grid_clicker/grid_clicker.py new file mode 100644 index 000000000..1f99c3c26 --- /dev/null +++ b/for_games/grid_clicker/grid_clicker.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import argparse +import threading +import time +import sys + +# pip install pyautogui==0.9.54 +import pyautogui + +# pip install pynput==1.8.1 +from pynput import mouse + +# pip install screeninfo==0.8.1 +from screeninfo import Monitor, get_monitors + + +pyautogui.FAILSAFE = False + +# Глобальный флаг работы +is_running = True + + +def on_click(x, y, button, pressed) -> bool | None: + global is_running + # Если нажата правая кнопка мыши (любая: нажатие или отпускание) + if button == mouse.Button.right: + print("\n[!] Обнаружен клик правой кнопкой. Останавливаю...") + is_running = False + return False # Останавливает сам слушатель pynput + + +def start_click_listener() -> None: + # Запуск прослушивания мыши в отдельном потоке + with mouse.Listener(on_click=on_click) as listener: + listener.join() + + +def click_all_on_screen( + step: int = 50, + do_click: bool = False, + sleep_time_before_starting_secs: int = 3, + # Режим 1: Прямые координаты + coords: tuple[int, int, int, int] | None = None, # (x1, y1, x2, y2) + # Режим 2: По номеру монитора + monitor_number: int = 1, + sleep_time_between_clicks_ms: int = 10, + offset_top: int = 50, # Защитный отступ сверху + offset_bottom: int = 50, # Защитный отступ снизу + offset_left: int = 50, # Защитный отступ слева + offset_right: int = 50, # Защитный отступ справа +) -> None: + global is_running + + if not do_click: + print("[!] ВНИМАНИЕ: Режим имитации включен, клики производиться не будут.") + + # Запуск потока для-прерывателя + threading.Thread(target=start_click_listener, daemon=True).start() + + # Режим 1: Прямые координаты + if coords: + start_x, start_y, end_x, end_y = coords + print(f"Режим ручных координат: X({start_x}..{end_x}), Y({start_y}..{end_y})") + + # Режим 2: По номеру монитора + else: + monitors = get_monitors() + try: + monitor: Monitor = monitors[monitor_number - 1] + except IndexError: + print(f"[#] Ошибка: Номер монитора должен быть от 1 до {len(monitors)}") + return + + # Определяем границы с учетом отступов + start_x = monitor.x + offset_left + end_x = monitor.x + monitor.width - offset_right + start_y = monitor.y + offset_top + end_y = monitor.y + monitor.height - offset_bottom + print(f"Режим монитора #{monitor_number} с отступами.") + + print(f"Область: X от {start_x} до {end_x}, Y от {start_y} до {end_y}") + print(f"Шаг: {step}px") + + sleep_time_between_clicks_secs: float = sleep_time_between_clicks_ms / 1000 + + time.sleep(sleep_time_before_starting_secs) + + print("Работаю. Нажми ПРАВУЮ кнопку мыши для стопа.") + + for y in range(start_y, end_y, step): + for x in range(start_x, end_x, step): + if not is_running: + print("Программа экстренно завершена.") + return + + pyautogui.moveTo(x, y) + if do_click: + pyautogui.click(x, y) + time.sleep(sleep_time_between_clicks_secs) + + print("Готово!") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="GridClicker: Скрипт для протыкивания экрана по сетке.", + # Этот параметр автоматически добавит (default: ...) в описание каждого аргумента + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + base_group = parser.add_argument_group("Основные настройки") + base_group.add_argument("-s", "--step", type=int, default=50, help="Шаг сетки") + base_group.add_argument( + "--do-click", + action="store_true", + help="РАЗРЕШИТЬ клики (без этого флага только перемещение курсора)", + ) + + base_group.add_argument( + "-t", + "--sleep_time_before_starting_secs", + type=int, + default=3, + dest="sleep_time_before_starting_secs", + help="Задержка перед началом (сек)", + ) + base_group.add_argument( + "-tc", + "--sleep", + type=int, + default=10, + dest="sleep_time_between_clicks_ms", + help="Задержка (мс)", + ) + + # Новая группа для координат + coord_group = parser.add_argument_group( + "Режим конкретных координат (игнорирует настройки монитора)" + ) + coord_group.add_argument("--x1", type=int, help="Левая граница") + coord_group.add_argument("--y1", type=int, help="Верхняя граница") + coord_group.add_argument("--x2", type=int, help="Правая граница") + coord_group.add_argument("--y2", type=int, help="Нижняя граница") + + monitor_group = parser.add_argument_group( + "Настройки монитора (используются, если не заданы x1,y1,x2,y2)" + ) + monitor_group.add_argument( + "-m", "--monitor", type=int, default=1, dest="monitor_number" + ) + monitor_group.add_argument("--top", type=int, default=50, dest="offset_top") + monitor_group.add_argument("--bottom", type=int, default=50, dest="offset_bottom") + monitor_group.add_argument("--left", type=int, default=50, dest="offset_left") + monitor_group.add_argument("--right", type=int, default=50, dest="offset_right") + + args = parser.parse_args() + + # Если скрипт запущен без аргументов — показываем help и выходим + if len(sys.argv) == 1: + parser.print_help() + sys.exit(0) + + # Собираем координаты в кортеж, если они все указаны + coords: tuple[int, int, int, int] | None = None + if all(v is not None for v in [args.x1, args.y1, args.x2, args.y2]): + coords = args.x1, args.y1, args.x2, args.y2 + + click_all_on_screen( + step=args.step, + do_click=args.do_click, + sleep_time_before_starting_secs=args.sleep_time_before_starting_secs, + sleep_time_between_clicks_ms=args.sleep_time_between_clicks_ms, + coords=coords, + monitor_number=args.monitor_number, + offset_top=args.offset_top, + offset_bottom=args.offset_bottom, + offset_left=args.offset_left, + offset_right=args.offset_right, + ) + + +if __name__ == "__main__": + # NOTE: Пример использования, режим прямых координат + # NOTE: grid_clicker.py --x1=-545 --y1=322 --x2=-42 --y2=1073 + # NOTE: grid_clicker.py --do-click --x1=-545 --y1=322 --x2=-42 --y2=1073 + # click_all_on_screen( + # step=100, + # coords=(-545, 322, -42, 1073), + # ) + + # NOTE: Пример использования, режим монитора + # click_all_on_screen( + # step=100, + # monitor_number=2, + # # sleep_time_between_clicks_ms=0.005, # 5 мс + # offset_bottom=150, + # ) + + main() diff --git a/for_games/grid_clicker/grid_clicker_gui_select_rect.py b/for_games/grid_clicker/grid_clicker_gui_select_rect.py new file mode 100644 index 000000000..eb5a8fff1 --- /dev/null +++ b/for_games/grid_clicker/grid_clicker_gui_select_rect.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import sys + +from pathlib import Path +from typing import Any + +from PyQt5.QtWidgets import ( + QApplication, + QDialog, + QFrame, + QPushButton, + QFormLayout, + QSpinBox, +) +from PyQt5.QtCore import Qt, QRect, QPoint +from PyQt5.QtGui import ( + QPainter, + QColor, + QPen, + QMouseEvent, + QKeyEvent, + QPaintEvent, + QCloseEvent, +) + +from grid_clicker import click_all_on_screen + +DIR: Path = Path(__file__).resolve().parent +PATH_SETTINGS: Path = DIR / "settings.json" + + +class AreaSelectorDialog(QDialog): + def __init__(self) -> None: + super().__init__() + + self.setWindowFlag(Qt.WindowType.FramelessWindowHint) + self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint) + + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + self.setCursor(Qt.CursorShape.CrossCursor) + + full_geometry: QRect = QApplication.desktop().geometry() + self.setGeometry(full_geometry) + + self._begin_global: QPoint = QPoint() # Глобальная точка начала + self._end_global: QPoint = QPoint() # Глобальная точка конца + self._is_selecting: bool = False + + self.selected_rect: QRect = QRect() + self.do_click: bool = True + self.need_run_clicker: bool = False + + self.spin_box_step = QSpinBox(value=50) + self.button_run = QPushButton("Run click", clicked=self.on_run_clicked) + self.button_run_test = QPushButton( + "Run TEST click", clicked=self.on_run_test_clicked + ) + self.button_close = QPushButton("Close", clicked=self.close) + + self.control_widget = QFrame(self) + self.control_widget.setCursor(Qt.ArrowCursor) + self.control_widget.setStyleSheet(""" + QFrame { + background-color: rgba(60, 60, 60, 200); + border-radius: 10px; + } + QCheckBox, + QLabel { + color: white; + } + """) + control_layout = QFormLayout(self.control_widget) + control_layout.addRow("&Step:", self.spin_box_step) + control_layout.addRow(self.button_run_test) + control_layout.addRow(self.button_run) + control_layout.addRow(self.button_close) + + if PATH_SETTINGS.exists(): + data: dict[str, Any] = json.loads(PATH_SETTINGS.read_text("UTF-8")) + coords: list[int] | None = data.get("coords") + if coords: + self.selected_rect.setCoords(*coords) + self.update() + + self._move_control_widget() + + def _move_control_widget(self): + if not self.selected_rect: + self.control_widget.hide() + return + + local_begin = self.mapFromGlobal(self.selected_rect.topLeft()) + self.control_widget.move(local_begin) + self.control_widget.show() + + @property + def step(self) -> int: + return self.spin_box_step.value() + + def on_run_test_clicked(self) -> None: + self.do_click = False + self.need_run_clicker = True + self.close() + + def on_run_clicked(self) -> None: + self.do_click = True + self.need_run_clicker = True + self.close() + + def mousePressEvent(self, event: QMouseEvent) -> None: + if event.button() == Qt.LeftButton: + self._begin_global = event.globalPos() + self._end_global = self._begin_global + + self._is_selecting = True + self._move_control_widget() + self.update() + + def mouseMoveEvent(self, event: QMouseEvent) -> None: + if self._is_selecting: + self._end_global = event.globalPos() + + rect = QRect(self._begin_global, self._end_global) + self.selected_rect = rect.normalized() + + self._move_control_widget() + self.update() + + def mouseReleaseEvent(self, event: QMouseEvent) -> None: + if self._is_selecting and event.button() == Qt.LeftButton: + self._is_selecting = False + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() == Qt.Key_Escape: + self.close() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.fillRect(self.rect(), QColor(0, 0, 0, 100)) + + if self.selected_rect: + local_rect: QRect = QRect( + self.mapFromGlobal(self.selected_rect.topLeft()), + self.mapFromGlobal(self.selected_rect.bottomRight()), + ) + + painter.setPen(QPen(Qt.red, 2, Qt.SolidLine)) + painter.fillRect(local_rect, QColor(255, 255, 255, 1)) + painter.drawRect(local_rect) + + def closeEvent(self, event: QCloseEvent) -> None: + if self.selected_rect: + data: dict[str, Any] = { + "coords": self.selected_rect.getCoords(), + } + PATH_SETTINGS.write_text( + json.dumps(data), + encoding="UTF-8", + ) + + super().closeEvent(event) + + +if __name__ == "__main__": + # Поддержка высокого DPI (важно для правильных координат на 4K мониторах) + QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) + + app = QApplication(sys.argv) + + selector = AreaSelectorDialog() + selector.exec() + + selected_rect: QRect = selector.selected_rect + if selector.selected_rect: + w = selected_rect.width() + h = selected_rect.height() + + # Извлекаем границы + x1 = selected_rect.left() + y1 = selected_rect.top() + x2 = selected_rect.right() + y2 = selected_rect.bottom() + + print("--- Координаты ---") + print(f"x1={x1}, y1={y1}, x2={x2}, y2={y2}") + print(f"Tuple: ({x1}, {y1}, {x2}, {y2})") + print(f"Args: --x1={x1} --y1={y1} --x2={x2} --y2={y2}") + print(f"Размер: {w}x{h}") + print("------------------------------") + + if selector.need_run_clicker: + click_all_on_screen( + do_click=selector.do_click, + step=selector.step, + sleep_time_before_starting_secs=1, + coords=(x1, y1, x2, y2), + ) + + else: + print("Выбор отменен.") + + # NOTE: Не нужен для QDialog.exec + # sys.exit(app.exec()) diff --git a/for_games/hacking__game__Rezrog/main.py b/for_games/hacking__game__Rezrog/main.py index f0a746591..964196897 100644 --- a/for_games/hacking__game__Rezrog/main.py +++ b/for_games/hacking__game__Rezrog/main.py @@ -50,26 +50,26 @@ import base64 import json -# pip install pycryptodome +# pip install pycryptodome==3.21.0 # OR: -# pip install pycryptodomex +# pip install pycryptodomex==3.21.0 from Crypto.Cipher import AES -# pip install pkcs7 +# pip install pkcs7==0.1.2 from pkcs7 import PKCS7Encoder -def encrypt(to_encrypt: str, key: str) -> bytes: +def encrypt(to_encrypt: str, key: bytes) -> bytes: encoder = PKCS7Encoder() encryptor = AES.new(key, AES.MODE_ECB) - pad_text = encoder.encode(to_encrypt) + pad_text = encoder.encode(to_encrypt).encode("utf-8") cipher = encryptor.encrypt(pad_text) return base64.b64encode(cipher) -def decrypt(to_decrypt: bytes, key: str) -> str: +def decrypt(to_decrypt: bytes, key: bytes) -> str: encoder = PKCS7Encoder() encryptor = AES.new(key, AES.MODE_ECB) @@ -80,7 +80,7 @@ def decrypt(to_decrypt: bytes, key: str) -> str: if __name__ == "__main__": - KEY = "32647863128053433215894456187813" + KEY = b"32647863128053433215894456187813" # C:\Users\\AppData\LocalLow\Soaphog\Rezrog\Save # C:\Users\\AppData\LocalLow\Soaphog\Rezrog\Save\game save.json @@ -91,11 +91,11 @@ def decrypt(to_decrypt: bytes, key: str) -> str: print(array) # b'o41r7i6/ekDVTWAmDvsmuEwhqMelq3bF3wukSbVpeeFaKIdyX ... - text = decrypt(array, KEY) + text: str = decrypt(array, KEY) print(text) # '{"goldEarnedTotal":23778,"timePlayedMage":3581.58911132813,"timePlayedArcher" ...' - data = json.loads(text) + data: dict = json.loads(text) print(data) # {'goldEarnedTotal': 23778, 'timePlayedMage': 3581.58911132813, ... @@ -112,7 +112,7 @@ def decrypt(to_decrypt: bytes, key: str) -> str: print() - new_array = encrypt(text, KEY) + new_array: bytes = encrypt(text, KEY) print(new_array) # b'o41r7i6/ekDVTWAmDvsmuEwhqMelq3bF3wukSbVpeeFaKIdyX ... with open("progress_encrypted.json", "wb") as f: diff --git a/for_games/hotkeys_for_cheats__Disciples_Sacred_Lands/main.py b/for_games/hotkeys_for_cheats__Disciples_Sacred_Lands/main.py index 41db2e1cc..027652d21 100644 --- a/for_games/hotkeys_for_cheats__Disciples_Sacred_Lands/main.py +++ b/for_games/hotkeys_for_cheats__Disciples_Sacred_Lands/main.py @@ -8,7 +8,7 @@ import keyboard -def input_command(text: str): +def input_command(text: str) -> None: keyboard.press_and_release("enter") keyboard.write(text) diff --git a/for_games/input_cheats_starcraft.py b/for_games/input_cheats_starcraft.py index 935349de0..6d9100edf 100644 --- a/for_games/input_cheats_starcraft.py +++ b/for_games/input_cheats_starcraft.py @@ -11,7 +11,7 @@ import keyboard -def write(text: str): +def write(text: str) -> None: print(text) # Удаление символа, который мог попасть при вводе хоткея, например Z, X, C или V diff --git a/for_games/input_cheats_starcraft2.py b/for_games/input_cheats_starcraft2.py index 73b45ea12..771ea44ce 100644 --- a/for_games/input_cheats_starcraft2.py +++ b/for_games/input_cheats_starcraft2.py @@ -13,7 +13,7 @@ import keyboard -def write(text: str): +def write(text: str) -> None: print(text) os.system(f"echo {text}|clip") diff --git a/html_parsing/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py b/for_games/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py similarity index 98% rename from html_parsing/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py rename to for_games/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py index 8c726dc48..92f0a55e1 100644 --- a/html_parsing/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py +++ b/for_games/kanobu_ru__games__collections__igry_s_podderzhkoi_rtx.py @@ -79,7 +79,7 @@ def get_games() -> list[Game]: return items -def save_as_html(file_name: str, items: list[Game], columns=4): +def save_as_html(file_name: str, items: list[Game], columns=4) -> None: title = "Games with RTX" with open(file_name, "w", encoding="utf-8") as f: diff --git a/minecraft__seed.py b/for_games/minecraft__seed.py similarity index 100% rename from minecraft__seed.py rename to for_games/minecraft__seed.py diff --git a/for_games/nintendo_switch_emulator__games/common.py b/for_games/nintendo_switch_emulator__games/common.py index b6d41906f..546b0d4cd 100644 --- a/for_games/nintendo_switch_emulator__games/common.py +++ b/for_games/nintendo_switch_emulator__games/common.py @@ -12,7 +12,7 @@ def get_json_file_name(file_name: str) -> str: return Path(file_name).name + ".json" -def dump(file_name: str, games: list, playable_games: list, perfect_games: list): +def dump(file_name: str, games: list, playable_games: list, perfect_games: list) -> None: data = { "perfect_games": { "total": len(perfect_games), diff --git a/for_games/press_release_keys__ScanCodes__for_games.py b/for_games/press_release_keys__ScanCodes__for_games.py index 4ce4075d3..c788e0e6d 100644 --- a/for_games/press_release_keys__ScanCodes__for_games.py +++ b/for_games/press_release_keys__ScanCodes__for_games.py @@ -219,7 +219,7 @@ class Input(ctypes.Structure): ] -def press_key(hex_key_code: int): +def press_key(hex_key_code: int) -> None: extra = ctypes.c_ulong(0) ii_ = Input_I() ii_.ki = KeyBdInput(0, hex_key_code, KEYEVENTF_SCANCODE, 0, ctypes.pointer(extra)) @@ -227,7 +227,7 @@ def press_key(hex_key_code: int): ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) -def release_key(hex_key_code: int): +def release_key(hex_key_code: int) -> None: extra = ctypes.c_ulong(0) ii_ = Input_I() ii_.ki = KeyBdInput( @@ -237,7 +237,7 @@ def release_key(hex_key_code: int): ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) -def write_key(hex_key_code: int, interval=0.1, pause=None): +def write_key(hex_key_code: int, interval=0.1, pause=None) -> None: press_key(hex_key_code) time.sleep(interval) release_key(hex_key_code) diff --git a/for_games/rbkgames_com__games__code_vein.py b/for_games/rbkgames_com__games__code_vein.py index 2bec8043e..e254dcda6 100644 --- a/for_games/rbkgames_com__games__code_vein.py +++ b/for_games/rbkgames_com__games__code_vein.py @@ -9,7 +9,7 @@ url = "https://rbkgames.com/games/code-vein/articles/kody-krovi-v-code-vein-kak-poluchit/" -rs = requests.get() +rs = requests.get(url) root = BeautifulSoup(rs.content, "html.parser") items = [] diff --git a/for_games/solutions of games/ELEX__hacking_minigame.py b/for_games/solutions of games/ELEX__hacking_minigame.py index 684003e25..8649ce898 100644 --- a/for_games/solutions of games/ELEX__hacking_minigame.py +++ b/for_games/solutions of games/ELEX__hacking_minigame.py @@ -16,7 +16,7 @@ def run( keys: list[int], fixed_values: list[str | int], need_values: list[str | int], -): +) -> None: while True: items = keys.copy() fixed_items = fixed_values.copy() diff --git a/for_games/solutions of games/FFXIII-2_clock_puzzle_solver/main.py b/for_games/solutions of games/FFXIII-2_clock_puzzle_solver/main.py index 41f559149..087fbfa68 100644 --- a/for_games/solutions of games/FFXIII-2_clock_puzzle_solver/main.py +++ b/for_games/solutions of games/FFXIII-2_clock_puzzle_solver/main.py @@ -27,7 +27,7 @@ def is_fail(clock_data: dict) -> bool: return a == 0 and b == 0 -def click_clock_item(clock_data: dict, selected_index: int): +def click_clock_item(clock_data: dict, selected_index: int) -> None: items = clock_data["items"] move_value = items[selected_index] @@ -48,7 +48,7 @@ def click_clock_item(clock_data: dict, selected_index: int): clock_data["history"].append(deepcopy(clock_data["items"])) -def solve_step(clock_data: dict, index: int, result_win: dict): +def solve_step(clock_data: dict, index: int, result_win: dict) -> None: # Если текущий индекс указывает на уже активированную кнопку if clock_data["items"][index] == 0: return @@ -95,7 +95,7 @@ def solver(clock_items: list) -> list: return list(wins.values()) -def print_extended(clock_data: dict): +def print_extended(clock_data: dict) -> None: num_clock_items = len(clock_data["selected_indexes"]) # Форматирование строки для красивого отображения двухзначных индексов @@ -111,7 +111,7 @@ def print_extended(clock_data: dict): print(fmt_print.format(item, index, value)) -def print_simple(clock_data: dict): +def print_simple(clock_data: dict) -> None: simple_result = zip(clock_data["selected_indexes"], clock_data["selected_values"]) print(" -> ".join(f"{index}({value})" for index, value in simple_result)) diff --git a/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/README.md b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/README.md new file mode 100644 index 000000000..506b7991f --- /dev/null +++ b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/README.md @@ -0,0 +1,4 @@ +Solution puzzle from game SpongeBob SquarePants: Battle for Bikini Bottom - Rehydrated +=========== + +![](screenshot.jpg) diff --git a/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/main.py b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/main.py new file mode 100644 index 000000000..f0c4b1f72 --- /dev/null +++ b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/main.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import copy + + +def activate(cylinders: list[bool], index: int) -> None: + left_index = (index - 1) % len(cylinders) + right_index = (index + 1) % len(cylinders) + + cylinders[left_index] = not cylinders[left_index] + cylinders[index] = not cylinders[index] + cylinders[right_index] = not cylinders[right_index] + + +def is_win(cylinders: list[bool]) -> bool: + return all(cylinders) + + +def run(init_cylinders: list[bool]) -> None: + for start_index in range(len(init_cylinders)): + seq = [start_index] + + cylinders = copy.deepcopy(init_cylinders) + activate(cylinders, start_index) + + for i in range(len(cylinders)): + if i == start_index: + continue + + seq.append(i) + activate(cylinders, i) + + if is_win(cylinders): + print(seq) + return + + +CYLINDERS: list[bool] = [False] * 8 + + +if __name__ == "__main__": + run(CYLINDERS) + # [0, 1, 2, 3, 4, 5, 6, 7] diff --git a/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/screenshot.jpg b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/screenshot.jpg new file mode 100644 index 000000000..e05f73485 Binary files /dev/null and b/for_games/solutions of games/SpongeBob SquarePants Battle for Bikini Bottom - Rehydrated. 8 cylinder puzzle/screenshot.jpg differ diff --git a/for_games/solutions of games/solution_puzzle_from_game_Afterfall_InSanity/main.py b/for_games/solutions of games/solution_puzzle_from_game_Afterfall_InSanity/main.py index d4d5e93fe..4db2fdf4b 100644 --- a/for_games/solutions of games/solution_puzzle_from_game_Afterfall_InSanity/main.py +++ b/for_games/solutions of games/solution_puzzle_from_game_Afterfall_InSanity/main.py @@ -14,7 +14,7 @@ class Button: is_clicked: bool = False -def activate(buttons: list[Button], index: int): +def activate(buttons: list[Button], index: int) -> None: button = buttons[index] if button.is_clicked: return @@ -37,7 +37,7 @@ def is_win(buttons: list[Button]) -> bool: return all(x.value for x in buttons) -def run(init_buttons: list[Button]): +def run(init_buttons: list[Button]) -> None: for start_index in range(len(init_buttons)): seq = [start_index] diff --git a/for_games/store_steampowered_com__check_early_access.py b/for_games/store_steampowered_com__check_early_access.py index 7f02dc041..c0c87a5f5 100644 --- a/for_games/store_steampowered_com__check_early_access.py +++ b/for_games/store_steampowered_com__check_early_access.py @@ -21,6 +21,8 @@ def is_early_access(app_id_or_url: int | str) -> bool: url = f"https://store.steampowered.com/app/{app_id}/" rs = session.get(url) + rs.raise_for_status() + return 'id="earlyAccess' in rs.text diff --git a/from_ghbdtn.py b/from_ghbdtn.py index 26f6f6493..c949d363c 100644 --- a/from_ghbdtn.py +++ b/from_ghbdtn.py @@ -5,18 +5,13 @@ def from_ghbdtn(text: str) -> str: - # SOURCE: https://ru.stackoverflow.com/a/812203/201445 - layout = dict( - zip( - map( - ord, - """qwertyuiop[]asdfghjkl;'zxcvbnm,./`QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~""", - ), - """йцукенгшщзхъфывапролджэячсмитьбю.ёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё""", - ) - ) - - return text.translate(layout) + en = """qwertyuiop[]asdfghjkl;'zxcvbnm,./`QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~""" + ru = """йцукенгшщзхъфывапролджэячсмитьбю.ёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё""" + + translator = dict(zip(en, ru)) + table = str.maketrans(translator) + + return text.translate(table) if __name__ == "__main__": diff --git a/function_example/function_example.py b/function_example/function_example.py index 26ce1880b..036887abe 100644 --- a/function_example/function_example.py +++ b/function_example/function_example.py @@ -8,12 +8,12 @@ def my_sum(*args): return s -def my_print(**kwargs): +def my_print(**kwargs) -> None: for name, value in kwargs.items(): print("%s=%s(%s)" % (name, value, type(value))) -def sql_insert(table, **kwargs): +def sql_insert(table, **kwargs) -> str: # Example: INSERT INTO users (login, pass) values('TestUser', '123456') columns = kwargs.keys() values = ["'%s'" % x for x in kwargs.values()] @@ -24,7 +24,7 @@ def sql_insert(table, **kwargs): ) -def say_hello(word="World", spl="!"): +def say_hello(word="World", spl="!") -> None: print("Hello, " + word + spl) diff --git a/functor.py b/functor.py index 554f8fa15..05f7a8834 100644 --- a/functor.py +++ b/functor.py @@ -8,7 +8,7 @@ class Foo: - def __call__(self): + def __call__(self) -> int: return 1 @@ -23,14 +23,14 @@ def __call__(self, a, b): class Bar: - def __init__(self, start_value=0): + def __init__(self, start_value=0) -> None: self.value = start_value def __call__(self, *args, **kwargs): self.value += 1 return self - def __str__(self): + def __str__(self) -> str: return f"" diff --git a/games/game__guess_the_number/gui__pyqt/main.py b/games/game__guess_the_number/gui__pyqt/main.py index 7be3bca80..55354089d 100644 --- a/games/game__guess_the_number/gui__pyqt/main.py +++ b/games/game__guess_the_number/gui__pyqt/main.py @@ -11,7 +11,7 @@ from PyQt5.Qt import * -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -25,14 +25,14 @@ def log_uncaught_exceptions(ex_cls, ex, tb): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/907a596654aabb9948934b68b0f7b1fe392e23bd/qt__pyqt__pyside__pyqode/pyqt__QListWidget__Flow.py class WrapListWidget(QListWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setFlow(QListView.LeftToRight) self.setWrapping(True) self.setUniformItemSizes(True) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) self.setWrapping(self.isWrapping()) @@ -41,7 +41,7 @@ def resizeEvent(self, event): class PageGuessWidget(QWidget): about_prev = pyqtSignal() - def __init__(self): + def __init__(self) -> None: super().__init__() self.lives = 10 @@ -71,11 +71,11 @@ def __init__(self): self._update_states() - def _update_states(self): + def _update_states(self) -> None: self.lbl_lives.setText(f"Жизней: {self.lives}") self.lbl_level.setText(f"Уровень: {self.level}") - def start_game(self, level: int, lives=10): + def start_game(self, level: int, lives=10) -> None: self.lives = lives self.level = level @@ -88,7 +88,7 @@ def start_game(self, level: int, lives=10): self._update_states() - def _on_number_clicked(self, item: QListWidgetItem): + def _on_number_clicked(self, item: QListWidgetItem) -> None: num = int(item.text()) if self.guess_number == num: @@ -122,7 +122,7 @@ def _on_number_clicked(self, item: QListWidgetItem): class PageSelectLevelWidget(QWidget): about_select_level = pyqtSignal(int) - def __init__(self): + def __init__(self) -> None: super().__init__() self.grid_layout = QGridLayout() @@ -140,7 +140,7 @@ def __init__(self): self.setLayout(layout) - def _add_levels(self, levels): + def _add_levels(self, levels) -> None: start_row = self.grid_layout.rowCount() for row, rows in enumerate(levels, start=start_row): @@ -154,7 +154,7 @@ def _add_levels(self, levels): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Guess the number") @@ -174,7 +174,7 @@ def __init__(self): self.setCentralWidget(self.stack_widget) - def _on_select_level(self, level: int): + def _on_select_level(self, level: int) -> None: # Переключаемся на страницу с игрой self.stack_widget.setCurrentWidget(self.page_guess) diff --git a/games/life/board.py b/games/life/board.py index 6aa894f76..606d9016b 100644 --- a/games/life/board.py +++ b/games/life/board.py @@ -25,7 +25,7 @@ class StepResultEnum(enum.Enum): class Board(QObject): on_update_generation = pyqtSignal(int) - def __init__(self): + def __init__(self) -> None: super().__init__() self.rows = 100 @@ -38,7 +38,7 @@ def __init__(self): self.last_step_result: StepResultEnum = None - def generate(self, seed: str = ""): + def generate(self, seed: str = "") -> None: self.seed = seed if not self.seed: self.seed = get_random_seed() @@ -62,7 +62,7 @@ def generation_number(self) -> int: return self.__generation_number @generation_number.setter - def generation_number(self, value: int): + def generation_number(self, value: int) -> None: self.__generation_number = value self.on_update_generation.emit(self.generation_number) diff --git a/games/life/main_console.py b/games/life/main_console.py index 8402c5fd0..decd1f919 100644 --- a/games/life/main_console.py +++ b/games/life/main_console.py @@ -20,7 +20,7 @@ class MyLateFigletText(StaticRenderer): - def __init__(self, rendered_text_func: Callable[[], str], **kwargs): + def __init__(self, rendered_text_func: Callable[[], str], **kwargs) -> None: super().__init__() self.rendered_text_func = rendered_text_func @@ -45,7 +45,7 @@ def __init__( height: int, width: int, next_scene: str, - ): + ) -> None: self.y = y self.x = x self.height = height @@ -70,7 +70,7 @@ def __init__( self.thread = Thread(target=self._run_timer, daemon=True) - def start(self, timer_ms: int, seed: str): + def start(self, timer_ms: int, seed: str) -> None: self.is_fail = False self.timer_secs = timer_ms / 1000 @@ -81,7 +81,7 @@ def start(self, timer_ms: int, seed: str): # TODO: RuntimeError: threads can only be started once self.thread.start() - def _run_timer(self): + def _run_timer(self) -> None: while self.board.do_step() == StepResultEnum.OK: time.sleep(self.timer_secs) @@ -124,7 +124,7 @@ def __init__( self, screen: Screen, y: int, x: int, height: int, width: int, - ): + ) -> None: self.y = y self.x = x self.height = height @@ -167,7 +167,7 @@ def get_timer_ms(self) -> int: def get_seed(self) -> str: return self.le_seed.value - def _start(self): + def _start(self) -> None: # TODO: validator не работает. Не помогает save и validate self.save(validate=True) @@ -178,7 +178,7 @@ def _start(self): ) -def demo(screen: Screen, scene: Scene): +def demo(screen: Screen, scene: Scene) -> None: menu_widget = MenuWidget( screen, y=0, x=0, diff --git a/games/life/main_gui.py b/games/life/main_gui.py index cebca1a08..0f1d28a1c 100644 --- a/games/life/main_gui.py +++ b/games/life/main_gui.py @@ -25,7 +25,7 @@ from board import Board, StepResultEnum, get_random_seed -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -41,13 +41,13 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class BoardWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.board = Board() self.cell_size = 1 - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) self.cell_size = min(self.width(), self.height()) // min( @@ -58,14 +58,14 @@ def resizeEvent(self, event): self.update() - def _draw_cell_board(self, painter: QPainter, x: int, y: int, color: QColor): + def _draw_cell_board(self, painter: QPainter, x: int, y: int, color: QColor) -> None: painter.setPen(Qt.NoPen) painter.setBrush(color) painter.drawRect( x * self.cell_size, y * self.cell_size, self.cell_size, self.cell_size ) - def _draw_board(self, painter: QPainter): + def _draw_board(self, painter: QPainter) -> None: painter.save() # Рисование заполненных ячеек @@ -78,7 +78,7 @@ def _draw_board(self, painter: QPainter): painter.restore() - def paintEvent(self, event: QPaintEvent): + def paintEvent(self, event: QPaintEvent) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -90,7 +90,7 @@ class MainWindow(QWidget): TITLE = "Life" - def __init__(self): + def __init__(self) -> None: super().__init__() self.board_widget = BoardWidget() @@ -138,16 +138,16 @@ def __init__(self): self.setLayout(main_layout) self._update_states() - def _do_rand_seed(self): + def _do_rand_seed(self) -> None: self.le_seed.setText(get_random_seed()) - def start(self): + def start(self) -> None: self.board.generate(seed=self.le_seed.text()) self.timer.stop() self.start_pause_timer() - def start_pause_timer(self): + def start_pause_timer(self) -> None: if self.timer.isActive(): self.timer.stop() else: @@ -155,7 +155,7 @@ def start_pause_timer(self): self._update_states() - def _update_states(self): + def _update_states(self) -> None: is_paused = not self.timer.isActive() prefix = "[IS PAUSED] " if is_paused else "" self.setWindowTitle( @@ -168,12 +168,12 @@ def _update_states(self): self.button_timer.setChecked(self.timer.isActive()) - def abort_game(self, reason: str): + def abort_game(self, reason: str) -> None: self.timer.stop() self._update_states() QMessageBox.information(self, "Информация", f"Проигрыш! {reason}") - def _on_logic(self): + def _on_logic(self) -> None: result = self.board.do_step() if result == StepResultEnum.OK: return @@ -190,7 +190,7 @@ def _on_logic(self): self.abort_game(reason) - def _on_tick(self): + def _on_tick(self) -> None: self._on_logic() self._update_states() self.board_widget.update() diff --git a/games/nim/main.py b/games/nim/main.py new file mode 100644 index 000000000..25d7b1a3c --- /dev/null +++ b/games/nim/main.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import random + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Optional + + +# TODO: +class FinishGameException(Exception): + pass + + +@dataclass +class Player(ABC): + name: str + game: Optional["Game"] = field(repr=False, default=None) + + @abstractmethod + def is_bot(self) -> bool: + pass + + @abstractmethod + def make_choice(self) -> int: + pass + + +class UserPlayer(Player): + def is_bot(self) -> bool: + return False + + def make_choice(self) -> int: + while True: + try: + stones = int( + input( + f"Игрок {self.name!r} выбирает камни от {self.game.MIN_STONES} до {self.game.MAX_STONES} " + f"(всего камней: {self.game.stones}): " + ) + ) + assert self.game.MIN_STONES <= stones <= self.game.MAX_STONES + + if stones > self.game.stones: + print("Нельзя камней выбрать больше, чем осталось!") + continue + + return stones + except (ValueError, AssertionError): + print("Неправильное значение!") + continue + + +class BotPlayer(Player): + def is_bot(self) -> bool: + return True + + def make_choice(self) -> int: + # Чтобы бот не выбирал очевидно проигрышное количество камней, когда можно победить + # Если осталось от 2 до 4 камней, то выбирать от 1 до 3 камней - чтобы остался только 1 камень + match self.game.stones: + case 1: + return 1 # Без вариантов, остается только проиграть + case 2: + return 1 + case 3: + return 2 + case 4: + return 3 + + return min( + self.game.stones, + random.randint(self.game.MIN_STONES, self.game.MAX_STONES), + ) + + +@dataclass +class Game: + player1: Player + player2: Player + + round: int = 0 + stones: int = 0 + is_finished: bool = False + + MIN_STONES: int = 1 + MAX_STONES: int = 3 + MAX_NUMBER: int = 21 + + def start(self) -> None: + self.player1.game = self + self.player2.game = self + + self.round = 0 + self.stones = self.MAX_NUMBER + + self.is_finished = False + + player1_is_first: bool = False + variants: list[str] = ["y", "n"] + + if self.player1.is_bot(): + print(f"Игрок {self.player1.name!r} подкидывает монетку") + player1_is_first = random.choice(variants) == variants[0] + else: + while True: + try: + value: str = input(f"Ты первый? ({'/'.join(variants)}): ").lower() + if not value: + value = variants[0] + assert value in variants + player1_is_first = value == variants[0] + break + + except AssertionError: + print("Неправильное значение!") + continue + + # Меняем местами первого и второго игроков + if not player1_is_first: + self.player1, self.player2 = self.player2, self.player1 + + print(f"Первым ходит игрок {self.player1.name!r}") + + def do_choice(self, player: Player): + stones: int = player.make_choice() + print(f"Игрок {player.name!r} выбрал: {stones}") + + self.stones -= stones + if self.stones <= 0: + # TODO: + raise FinishGameException(f"Игрок {player.name!r} проиграл!") + + # TODO: Немного ASCII для удобного просмотра + def move(self) -> bool: + self.round += 1 + + print(f"\nРаунд #{self.round}. Камней: {self.stones}") + + try: + self.do_choice(self.player1) + self.do_choice(self.player2) + + except FinishGameException as e: + print() + print(e) + self.is_finished = True + + return not self.is_finished + + +if __name__ == "__main__": + # game = Game( + # player1=BotPlayer("Бот1"), + # player2=BotPlayer("Бот2"), + # ) + + # game = Game( + # player1=UserPlayer("Человек1"), + # player2=UserPlayer("Человек2"), + # ) + + game = Game( + player1=UserPlayer("Человек"), + player2=BotPlayer("Бот"), + ) + + game.start() + + while game.move(): + pass diff --git a/games/tetris/common.py b/games/tetris/common.py deleted file mode 100644 index e2df29604..000000000 --- a/games/tetris/common.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = "ipetrash" - - -import logging - -from config import DEBUG - - -logging.basicConfig( - level=logging.DEBUG if DEBUG else logging.WARNING, - format="[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", -) -logger = logging.getLogger(__name__) diff --git a/games/tetris/etc/screenshot_gui.png b/games/tetris/etc/screenshot_gui.png index d05bc61e4..f54781692 100644 Binary files a/games/tetris/etc/screenshot_gui.png and b/games/tetris/etc/screenshot_gui.png differ diff --git a/games/tetris/main_console.py b/games/tetris/main_console.py index b078b8aa0..ece99506f 100644 --- a/games/tetris/main_console.py +++ b/games/tetris/main_console.py @@ -17,8 +17,8 @@ from asciimatics.widgets import Frame from asciimatics.exceptions import StopApplication, NextScene, ResizeScreenError -from board import Board -from piece import PieceO, PieceI, PieceS, PieceZ, PieceL, PieceJ, PieceT +from src.core.board import Board +from src.core.piece import PieceO, PieceI, PieceS, PieceZ, PieceL, PieceJ, PieceT # SOURCE: https://asciimatics.readthedocs.io/en/stable/io.html#colours @@ -35,7 +35,7 @@ class MyLateFigletText(StaticRenderer): - def __init__(self, rendered_text_func: Callable[[], str], **kwargs): + def __init__(self, rendered_text_func: Callable[[], str], **kwargs) -> None: super().__init__() self.rendered_text_func = rendered_text_func @@ -60,7 +60,7 @@ def __init__( height: int, width: int, next_scene: str, - ): + ) -> None: self.y = y self.x = x self.height = height @@ -85,13 +85,13 @@ def __init__( self.thread = Thread(target=self._run_timer, daemon=True) self.thread.start() - def _run_timer(self): + def _run_timer(self) -> None: while self.board.do_step(): time.sleep(0.3) self.is_fail = True - def update(self, frame_no: int): + def update(self, frame_no: int) -> None: super().update(frame_no) if self.is_fail: @@ -155,8 +155,11 @@ def __init__( self, board: Board, screen: Screen, - y: int, x: int, height: int, width: int, - ): + y: int, + x: int, + height: int, + width: int, + ) -> None: self.y = y self.x = x self.height = height @@ -175,7 +178,7 @@ def __init__( self.board = board self.next_piece = self.board.next_piece - def update(self, frame_no: int): + def update(self, frame_no: int) -> None: super().update(frame_no) self.next_piece = self.board.next_piece @@ -201,8 +204,11 @@ def __init__( self, board: Board, screen: Screen, - y: int, x: int, height: int, width: int, - ): + y: int, + x: int, + height: int, + width: int, + ) -> None: self.y = y self.x = x self.height = height @@ -220,7 +226,7 @@ def __init__( self.set_theme("monochrome") self.board = board - def update(self, frame_no: int): + def update(self, frame_no: int) -> None: super().update(frame_no) self.screen.print_at(f"Score: {self.board.score}", self.x, self.y) @@ -233,7 +239,7 @@ def frame_update_count(self) -> int: return 5 -def demo(screen: Screen, scene: Scene): +def demo(screen: Screen, scene: Scene) -> None: board = Board() scenes = [ Scene( diff --git a/games/tetris/main_gui.py b/games/tetris/main_gui.py index fe2077e46..3e7685fab 100644 --- a/games/tetris/main_gui.py +++ b/games/tetris/main_gui.py @@ -4,20 +4,42 @@ __author__ = "ipetrash" +import json +import string import sys +import random import traceback -from PyQt5.QtWidgets import QWidget, QApplication, QMessageBox -from PyQt5.QtGui import QPainter, QPaintEvent, QKeyEvent, QColor -from PyQt5.QtCore import Qt, QTimer - -from board import Board -from config import DEBUG -from common import logger -from piece import Piece - - -def log_uncaught_exceptions(ex_cls, ex, tb): +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QKeyEvent +from PyQt5.QtWidgets import ( + QWidget, + QApplication, + QMessageBox, + QVBoxLayout, + QHBoxLayout, + QLabel, + QGridLayout, + QToolButton, + QPushButton, + QScrollArea, + QCheckBox, + QStyle, + QLineEdit, +) + +from src.core.common import logger, ms_to_str +from src.gui.common import StatusGameEnum +from src.gui.board_widget import BoardWidget +from src.gui.piece_widget import PieceWidget + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -29,148 +51,271 @@ def log_uncaught_exceptions(ex_cls, ex, tb): sys.excepthook = log_uncaught_exceptions -class MainWindow(QWidget): - CELL_SIZE = 20 - SPEED_MS = 400 +# SOURCE: https://github.com/gil9red/parse_jira_logged_time/blob/9a842ed071f317b7323b7b7775aa9f5195dbaf47/widgets/__init__.py#L218 +def get_scroll_area(widget: QWidget) -> QScrollArea: + scroll_area = QScrollArea() + scroll_area.setFrameStyle(QScrollArea.NoFrame) + scroll_area.setWidgetResizable(True) + scroll_area.setWidget(widget) - TITLE = "Tetris" + return scroll_area - def __init__(self): - super().__init__() - self.board = Board() - self.board.on_update_score.connect(self._update_states) +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/521cec6ee5915a23064617b17a8c9258f4592a46/games/life/board.py#L14 +def get_random_seed(length: int = 8) -> str: + return "".join(random.choices(string.ascii_letters + string.digits, k=length)) - self.current_piece: Piece = None - self.next_piece: Piece = None - self.timer = QTimer() - self.timer.timeout.connect(self._on_tick) - self.timer.setInterval(self.SPEED_MS) - self.timer.start() +DIR: Path = Path(__file__).resolve().parent +FILE_HIGH_SCORES: Path = DIR / "high_scores.json" - self._update_states() - def _update_states(self): - self.setWindowTitle(f"{self.TITLE}. Score: {self.board.score}") +@dataclass +class HighScore: + date_added: datetime + time: str + score: int + + @classmethod + def parse(cls, data: dict[str, Any]) -> "HighScore": + return cls( + date_added=datetime.fromisoformat(data["date_added"]), + time=data["time"], + score=data["score"], + ) - def abort_game(self): - self.timer.stop() - QMessageBox.information(self, "Информация", "Проигрыш!") + def to_dict(self) -> dict[str, Any]: + return { + "date_added": self.date_added.isoformat(), + "time": self.time, + "score": self.score, + } - def _on_logic(self): - if not self.board.do_step(): - self.abort_game() - return - self.current_piece = self.board.current_piece - self.next_piece = self.board.next_piece +class MainWindow(QWidget): + TITLE: str = "Tetris" - def _on_tick(self): - self._on_logic() - self._update_states() - self.update() + def __init__(self) -> None: + super().__init__() - def _draw_cell_board(self, painter: QPainter, x: int, y: int, color: QColor): - painter.setPen(Qt.NoPen) - painter.setBrush(color) - painter.drawRect( - x * self.CELL_SIZE, y * self.CELL_SIZE, self.CELL_SIZE, self.CELL_SIZE + self.high_scores: list[HighScore] = [] + + self.high_scores_view = QLabel() + + self.next_piece_widget = PieceWidget() + self.score_label = QLabel() + self.playing_time_label = QLabel() + + self.board_widget = BoardWidget() + self.board_widget.board.on_next_piece.connect(self.next_piece_widget.set_piece) + self.board_widget.board.on_update_score.connect(self._update_states) + self.board_widget.on_before_start.connect(self._on_before_start) + self.board_widget.on_tick.connect(self._update_states) + self.board_widget.on_finish.connect(self._on_finish) + + def _add_button(text: str, key: Qt.Key) -> QToolButton: + button = QToolButton() + button.setText(text) + button.clicked.connect(lambda: self.board_widget.process_key(key)) + return button + + self.up_button = _add_button("🢁", Qt.Key_Up) + self.left_button = _add_button("🢀", Qt.Key_Left) + self.right_button = _add_button("🢂", Qt.Key_Right) + self.down_button = _add_button("🢃", Qt.Key_Down) + self.pause_resume_button = _add_button("Pause/Resume", Qt.Key_Space) + + control_layout = QGridLayout() + control_layout.setAlignment(Qt.AlignCenter) + control_layout.addWidget(self.up_button, 0, 1) + control_layout.addWidget(self.left_button, 1, 0) + control_layout.addWidget(self.right_button, 1, 2) + control_layout.addWidget(self.down_button, 2, 1) + control_layout.addWidget(self.pause_resume_button, 3, 0, 1, 4) + + self.start_button = QPushButton("START") + self.start_button.clicked.connect(self._do_start) + + self.cb_random = QCheckBox("Random") + + self.le_seed = QLineEdit(get_random_seed()) + self.le_seed.setVisible(self.cb_random.isChecked()) + self.le_seed.returnPressed.connect(self._do_start) + action_rand_seed = self.le_seed.addAction( + self.style().standardIcon(QStyle.SP_BrowserReload), + QLineEdit.TrailingPosition, + ) + action_rand_seed.triggered.connect( + lambda: self.le_seed.setText(get_random_seed()) ) - def _draw_board(self, painter: QPainter): - painter.save() - - # Рисование заполненных ячеек - for y, row in enumerate(self.board.matrix): - for x, cell_color in enumerate(row): - if not cell_color: - continue + self.cb_random.clicked.connect(self.le_seed.setVisible) + + right_layout = QVBoxLayout() + right_layout.addWidget(self.next_piece_widget) + right_layout.addWidget(self.score_label) + right_layout.addWidget(self.playing_time_label) + right_layout.addWidget(self.start_button) + right_layout.addWidget(self.cb_random) + right_layout.addWidget(self.le_seed) + right_layout.addLayout(control_layout) + right_layout.addWidget( + QLabel( + """ + + + + + +
ENTER - Start
SPACE - Pause/Resume
🢁 - Rotate🢀 - Left
🢂 - Right🢃 - Down
+

Scores by rows:

+ + + +
1: 1002: 300
3: 7004: 1500
+ """ + ) + ) + right_layout.addWidget(self.high_scores_view) + right_layout.addStretch() - self._draw_cell_board(painter, x, y, cell_color) + widget_right = QWidget() + widget_right.setLayout(right_layout) - painter.setPen(Qt.black) + main_layout = QHBoxLayout(self) + main_layout.addWidget(self.board_widget, stretch=1) + main_layout.addWidget(get_scroll_area(widget_right)) - # Горизонтальные линии - y1, y2 = 0, 0 - for i in range(self.board.ROWS + 1): - painter.drawLine(0, y1, self.CELL_SIZE * self.board.COLS, y2) - y1 += self.CELL_SIZE - y2 += self.CELL_SIZE + for w in self.findChildren(QWidget): + # У BoardWidget и поля ввода нужен фокус для взаимодействия + if w in [self.board_widget, self.le_seed]: + continue - # Вертикальные линии - x1, x2 = 0, 0 - for i in range(self.board.COLS + 1): - painter.drawLine(x1, 0, x2, self.CELL_SIZE * self.board.ROWS) - x1 += self.CELL_SIZE - x2 += self.CELL_SIZE + w.setFocusPolicy(Qt.FocusPolicy.NoFocus) - painter.restore() + self.load_high_scores() - def _draw_current_piece(self, painter: QPainter): - if not self.current_piece: - return + self._update_states() - painter.save() + def _on_before_start(self) -> None: + self.board_widget.seed = None + if self.cb_random.isChecked(): + self.board_widget.seed = self.le_seed.text() - for x, y in self.current_piece.get_points(): - self._draw_cell_board(painter, x, y, self.current_piece.get_color()) + def _do_start(self) -> None: + self.board_widget.start() - # Рисуем центр фигуры - if DEBUG: - self._draw_cell_board( - painter, self.current_piece.x, self.current_piece.y, Qt.black + def _on_finish(self) -> None: + self.high_scores.append( + HighScore( + date_added=datetime.now(), + time=ms_to_str(self.board_widget.playing_time_ms), + score=self.board_widget.board.score, ) + ) - x_next = self.board.COLS + 3 - y_next = 1 - for x, y in self.next_piece.get_points_for_state(x=x_next, y=y_next): - self._draw_cell_board(painter, x, y, self.next_piece.get_color()) + self._fill_high_scores_view() + self.save_high_scores() - painter.restore() + self._update_states() - def _draw_score(self, painter: QPainter): - painter.save() + def get_top_high_scores(self, number: int = 5) -> list[HighScore]: + # Сортировка по убыванию очков и возрастанию времени + self.high_scores.sort( + key=lambda obj: (obj.score, -(int(obj.time.replace(":", "")))), + reverse=True, + ) + if len(self.high_scores) > number: + self.high_scores = self.high_scores[:number] + return self.high_scores + + def _update_states(self) -> None: + score: int = self.board_widget.board.score + playing_time = ms_to_str(self.board_widget.playing_time_ms) + + title_parts = [self.TITLE] + + if self.board_widget.status != StatusGameEnum.INITED: + title_parts.append(f"Score: {score}") + title_parts.append(f"Time: {playing_time}") + + if self.board_widget.status == StatusGameEnum.PAUSED: + title_parts.append("Paused") + + self.setWindowTitle(". ".join(title_parts)) + self.score_label.setText(f"Score: {score}") + self.playing_time_label.setText(f"Time: {playing_time}") + + is_inited_or_finished: bool = self.board_widget.status in [ + StatusGameEnum.INITED, + StatusGameEnum.FINISHED, + ] + self.start_button.setEnabled(is_inited_or_finished) + self.cb_random.setEnabled(is_inited_or_finished) + self.le_seed.setEnabled(is_inited_or_finished) + + is_started: bool = self.board_widget.status == StatusGameEnum.STARTED + self.up_button.setEnabled(is_started) + self.left_button.setEnabled(is_started) + self.right_button.setEnabled(is_started) + self.down_button.setEnabled(is_started) + + self.pause_resume_button.setEnabled( + is_started or self.board_widget.status == StatusGameEnum.PAUSED + ) - painter.setPen(Qt.black) + def _fill_high_scores_view(self) -> None: + lines = ["High scores (top 5):"] - x, y = self.CELL_SIZE * (self.board.COLS + 1), self.CELL_SIZE * 5 - painter.drawText(x, y, f"Score: {self.board.score}") + for i, high_score in enumerate(self.get_top_high_scores(), start=1): + lines.append( + f"{i}. {high_score.score} - {high_score.time}
" + f"    {high_score.date_added:%d.%m.%Y %H:%M:%S}" + ) - painter.restore() + self.high_scores_view.setText("
".join(lines)) - def paintEvent(self, event: QPaintEvent): - painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) + def get_raw_high_scores(self) -> list[dict[str, Any]]: + return [high_score.to_dict() for high_score in self.high_scores] - self._draw_board(painter) - self._draw_current_piece(painter) - self._draw_score(painter) + def set_raw_high_scores(self, items: list[dict[str, Any]]) -> None: + self.high_scores = [HighScore.parse(data) for data in items] + self._fill_high_scores_view() - def keyReleaseEvent(self, event: QKeyEvent): - match event.key(): - case Qt.Key_Left if self.current_piece: - if self.current_piece.move_left(): - self.update() + def save_high_scores(self) -> None: + FILE_HIGH_SCORES.write_text( + json.dumps(self.get_raw_high_scores(), indent=4), + encoding="utf-8", + ) - case Qt.Key_Right if self.current_piece: - if self.current_piece.move_right(): - self.update() + def load_high_scores(self) -> None: + if not FILE_HIGH_SCORES.exists(): + return - case Qt.Key_Up if self.current_piece: - if self.current_piece.turn(): - self.update() + items: list[dict[str, Any]] = json.loads( + FILE_HIGH_SCORES.read_text(encoding="utf-8") + ) + self.set_raw_high_scores(items) - case Qt.Key_Down if self.current_piece: - while self.current_piece.move_down(): - self.update() + def keyReleaseEvent(self, event: QKeyEvent) -> None: + self.board_widget.process_key(event.key()) + self._update_states() if __name__ == "__main__": app = QApplication([]) mw = MainWindow() - mw.resize(360, 480) mw.show() + # mw = QWidget() + # main_layout = QHBoxLayout(mw) + # + # for cls in Piece.__subclasses__(): + # piece_widget = PieceWidget() + # piece_widget.set_piece(cls(0, 0)) + # main_layout.addWidget(piece_widget) + # + # mw.show() + app.exec() diff --git a/games/tetris/src/__init__.py b/games/tetris/src/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/games/tetris/src/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/games/tetris/src/core/__init__.py b/games/tetris/src/core/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/games/tetris/src/core/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/games/tetris/board.py b/games/tetris/src/core/board.py similarity index 60% rename from games/tetris/board.py rename to games/tetris/src/core/board.py index 8ae11581d..6f4da0d30 100644 --- a/games/tetris/board.py +++ b/games/tetris/src/core/board.py @@ -7,45 +7,61 @@ from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QColor -from common import logger -from piece import Piece +from .common import logger +from .piece import Piece class Board(QObject): - ROWS = 20 - COLS = 10 + ROWS: int = 20 + COLS: int = 10 + on_next_piece = pyqtSignal(Piece) on_update_score = pyqtSignal(int) - def __init__(self): + def __init__(self) -> None: super().__init__() self.matrix: list[list[QColor | None]] = [ - [None for _ in range(self.COLS)] - for _ in range(self.ROWS) + [None for _ in range(self.COLS)] for _ in range(self.ROWS) ] - self.current_piece: Piece = None - self.next_piece: Piece = None + self.current_piece: Piece | None = None + self.__next_piece: Piece | None = None self.__score: int = 0 + @property + def next_piece(self) -> Piece | None: + return self.__next_piece + + @next_piece.setter + def next_piece(self, value: Piece | None) -> None: + self.__next_piece = value + self.on_next_piece.emit(value) + @property def score(self) -> int: return self.__score @score.setter - def score(self, value: int): + def score(self, value: int) -> None: self.__score = value - self.on_update_score.emit(self.score) + self.on_update_score.emit(value) - def add_piece(self, piece: Piece): + def add_piece(self, piece: Piece) -> None: logger.debug("[add_piece]") for x, y in piece.get_points(): self.matrix[y][x] = piece.get_color() - def _do_make_collapse_of_rows(self): - to_delete = [] + def clear(self) -> None: + self.score = 0 + + for row in self.matrix: + for i in range(len(row)): + row[i] = None + + def _do_make_collapse_of_rows(self) -> None: + to_delete: list[list[QColor | None]] = [] for row in reversed(self.matrix): if all(color for color in row): to_delete.append(row) @@ -58,7 +74,7 @@ def _do_make_collapse_of_rows(self): if to_delete: self.score += {1: 100, 2: 300, 3: 700, 4: 1500}[len(to_delete)] - def _get_random_piece(self) -> Piece: + def get_random_piece(self) -> Piece: return Piece.get_random( x=self.COLS // 2, # По-умолчанию, по центру y=0, @@ -70,9 +86,9 @@ def do_step(self) -> bool: if self.next_piece: self.current_piece = self.next_piece else: - self.current_piece = self._get_random_piece() + self.current_piece = self.get_random_piece() - self.next_piece = self._get_random_piece() + self.next_piece = self.get_random_piece() # Если сразу после создания столкновение if self.current_piece.is_collapse(): diff --git a/games/tetris/src/core/common.py b/games/tetris/src/core/common.py new file mode 100644 index 000000000..ab623d839 --- /dev/null +++ b/games/tetris/src/core/common.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from .config import DEBUG + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/0360c558f85c0fe5e7320d88f90c0a4e23a7e342/seconds_to_str.py +def seconds_to_str(seconds: int | float) -> str: + hh, mm = divmod(seconds, 3600) + mm, ss = divmod(mm, 60) + return "%02d:%02d:%02d" % (hh, mm, ss) + + +def ms_to_str(ms: int | float) -> str: + return seconds_to_str(ms / 1000) + + +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("tetris") +logger.setLevel(logging.DEBUG if DEBUG else logging.WARNING) +logger.addHandler(default_handler) diff --git a/_FOO_TEST_TEST/parse_web_outlook_calendar/config.py b/games/tetris/src/core/config.py similarity index 63% rename from _FOO_TEST_TEST/parse_web_outlook_calendar/config.py rename to games/tetris/src/core/config.py index f93cf9f8a..c1e00b0be 100644 --- a/_FOO_TEST_TEST/parse_web_outlook_calendar/config.py +++ b/games/tetris/src/core/config.py @@ -4,5 +4,4 @@ __author__ = "ipetrash" -LOGIN = "" -PASSWORD = "" +DEBUG: bool = False diff --git a/games/tetris/piece.py b/games/tetris/src/core/piece.py similarity index 92% rename from games/tetris/piece.py rename to games/tetris/src/core/piece.py index bc757918b..1abdbc52a 100644 --- a/games/tetris/piece.py +++ b/games/tetris/src/core/piece.py @@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor -from common import logger +from .common import logger class Piece(abc.ABC): @@ -21,7 +21,7 @@ class Piece(abc.ABC): 4: 1, } - def __init__(self, x: int, y: int, parent: "Board" = None): + def __init__(self, x: int, y: int, parent: "Board" = None) -> None: self.x = x self.y = y self.parent = parent @@ -32,7 +32,8 @@ def __init__(self, x: int, y: int, parent: "Board" = None): @classmethod def get_random( cls, - x: int, y: int, + x: int, + y: int, parent: "Board", rand_x: bool = False, ) -> "Piece": @@ -101,12 +102,14 @@ def get_points_for_state( for i, row in enumerate(points_board): for j, value in enumerate(row): if value != ".": - points.append(( - # Рассчитываем разницу между X и остальными значениями - # и прибавляем текущие координаты центра фигуры - (j - idx_x) + x, - (i - idx_y) + y - )) + points.append( + ( + # Рассчитываем разницу между X и остальными значениями + # и прибавляем текущие координаты центра фигуры + (j - idx_x) + x, + (i - idx_y) + y, + ) + ) return points def get_min_x(self) -> int: @@ -121,10 +124,10 @@ def get_min_y(self) -> int: def get_max_y(self) -> int: return max(y for _, y in self.get_points()) - def go_to_next_state(self): + def go_to_next_state(self) -> None: self.current_state = self.STATES[self.current_state] - def set_points(self, points: list[tuple[int, int]]): + def set_points(self, points: list[tuple[int, int]]) -> None: self.points = points def get_points(self) -> list[tuple[int, int]]: @@ -235,7 +238,7 @@ def _get_all_states(self) -> list[list[str]]: return self._ def get_color(self) -> QColor: - return QColor(Qt.cyan) + return QColor("#7F00FF") # Violet class PieceS(Piece): diff --git a/games/tetris/src/gui/__init__.py b/games/tetris/src/gui/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/games/tetris/src/gui/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/games/tetris/src/gui/board_widget.py b/games/tetris/src/gui/board_widget.py new file mode 100644 index 000000000..f93d80b16 --- /dev/null +++ b/games/tetris/src/gui/board_widget.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import random + +from PyQt5.QtCore import pyqtSignal, QTimer, QSize, Qt +from PyQt5.QtGui import QPainter, QColor, QResizeEvent, QPaintEvent +from PyQt5.QtWidgets import QWidget + +from ..core.board import Board +from ..core.common import ms_to_str +from .common import CELL_SIZE, StatusGameEnum, painter_context, draw_cell_board + + +class BoardWidget(QWidget): + INDENT: int = 1 + SPEED_MS: int = 400 + + on_tick = pyqtSignal() + on_before_start = pyqtSignal() + on_finish = pyqtSignal() + + def __init__(self) -> None: + super().__init__() + + self.board = Board() + self.seed: str | None = None + + self.cell_size: int = CELL_SIZE + + self.__timer = QTimer() + self.__timer.timeout.connect(self._on_tick) + self.__timer.setInterval(self.SPEED_MS) + + self.playing_time_ms: int = 0 + self.__status = StatusGameEnum.INITED + + def minimumSizeHint(self) -> QSize: + columns = self.board.COLS + width = (CELL_SIZE * columns) + (self.INDENT * 2) + + rows = self.board.ROWS + height = (CELL_SIZE * rows) + (self.INDENT * 2) + return QSize(width, height) + + def _fill_random(self) -> None: + while True: + try: + self.board.clear() + self.board.current_piece = self.board.get_random_piece() + self.board.next_piece = self.board.get_random_piece() + + while True: + filled_cells = sum( + sum(1 for cell in row if cell is not None) + for row in self.board.matrix + ) + # Заполнили поле на нужное количество ячеек + if filled_cells >= 40: + return + + # С рандомом не повезло нужно заново заполнить + if not self.board.do_step(): + break + + if not self.board.current_piece: + continue + + for _ in range(random.randrange(0, self.board.COLS)): + match random.randint(1, 4): + case 1: + self.board.current_piece.move_left() + case 2: + self.board.current_piece.move_right() + case 3: + self.board.current_piece.turn() + case 4: + while self.board.current_piece.move_down(): + pass + + finally: + self.board.score = 0 + + def start(self) -> None: + if self.status not in [StatusGameEnum.INITED, StatusGameEnum.FINISHED]: + return + + self.status = StatusGameEnum.INITED + + self.on_before_start.emit() + + random.seed(self.seed) + if self.seed: + self._fill_random() + + self.status = StatusGameEnum.STARTED + self.update() + + @property + def status(self) -> StatusGameEnum: + return self.__status + + @status.setter + def status(self, value: StatusGameEnum) -> None: + if self.__status == value: + return + + self.__status = value + + match value: + case StatusGameEnum.INITED: + self.__timer.stop() + self.board.clear() + self.playing_time_ms = 0 + + case StatusGameEnum.STARTED: + self.__timer.start() + + case StatusGameEnum.PAUSED: + self.__timer.stop() + + case StatusGameEnum.FINISHED: + self.__timer.stop() + self.on_finish.emit() + + def abort_game(self) -> None: + self.status = StatusGameEnum.FINISHED + + def _on_logic(self) -> None: + if not self.board.do_step(): + self.abort_game() + + def _on_tick(self) -> None: + self._on_logic() + + self.update() + + self.playing_time_ms += self.__timer.interval() + self.on_tick.emit() + + @painter_context + def _draw_board(self, painter: QPainter) -> None: + # Рисование заполненных ячеек + for y, row in enumerate(self.board.matrix): + for x, cell_color in enumerate(row): + if not cell_color: + continue + + draw_cell_board( + painter, + x, + y, + cell_color, + cell_size=self.cell_size, + indent=self.INDENT, + ) + + painter.setPen(Qt.black) + + # Горизонтальные линии + y1 = y2 = self.INDENT + x1 = self.INDENT + x2 = self.cell_size * self.board.COLS + self.INDENT + for i in range(self.board.ROWS + 1): + painter.drawLine(x1, y1, x2, y2) + y1 += self.cell_size + y2 += self.cell_size + + # Вертикальные линии + x1 = x2 = self.INDENT + y1 = self.INDENT + y2 = self.cell_size * self.board.ROWS + self.INDENT + for i in range(self.board.COLS + 1): + painter.drawLine(x1, y1, x2, y2) + x1 += self.cell_size + x2 += self.cell_size + + @painter_context + def _draw_current_piece(self, painter: QPainter) -> None: + if not self.board.current_piece: + return + + for x, y in self.board.current_piece.get_points(): + draw_cell_board( + painter, + x, + y, + self.board.current_piece.get_color(), + cell_size=self.cell_size, + indent=self.INDENT, + ) + + @painter_context + def _draw_shadow_of_current_piece(self, painter: QPainter) -> None: + if not self.board.current_piece: + return + + points: list[tuple[int, int]] = self.board.current_piece.get_points() + + color: QColor = self.board.current_piece.get_color() + color.setAlphaF(0.2) + + # Рисование тени + for y, row in enumerate(self.board.matrix): + for x, _ in enumerate(row): + max_piece_y = max((py for px, py in points if px == x), default=-1) + + min_field_y = -1 + for y_, row_ in enumerate(self.board.matrix): + cell_color: QColor | None = row_[x] + if not cell_color: + continue + + min_field_y = y_ + break + + if y <= max_piece_y or (min_field_y != -1 and y >= min_field_y): + continue + + if ( + x < self.board.current_piece.get_min_x() + or x > self.board.current_piece.get_max_x() + ): + continue + + draw_cell_board( + painter, x, y, color, cell_size=self.cell_size, indent=self.INDENT + ) + + @painter_context + def _draw_glass(self, painter: QPainter) -> None: + match self.status: + case StatusGameEnum.INITED: + text = "Press START" + case StatusGameEnum.PAUSED: + text = "PAUSE" + case StatusGameEnum.FINISHED: + playing_time = ms_to_str(self.playing_time_ms) + text = f"FINISHED\nScore: {self.board.score}\nTime: {playing_time}" + case _: + return + + # Алгоритм изменения размера текста взят из http://stackoverflow.com/a/2204501 + # Для текущего пришлось немного адаптировать + max_line_width = max( + painter.fontMetrics().width(line) for line in text.splitlines() + ) + factor = min(self.width(), self.height()) / max_line_width + if factor < 1 or factor > 1.25: + f = painter.font() + + # NOTE: Замечено, что или pointSizeF, или pixelSize будет =1 + if f.pointSizeF() > 0: + point_size = f.pointSizeF() * factor + if point_size > 0: + f.setPointSizeF(point_size) + else: + pixel_size = int(f.pixelSize() * factor) + if pixel_size > 0: + f.setPixelSize(pixel_size) + + painter.setFont(f) + + painter.setPen(Qt.black) + + brush = QColor(Qt.lightGray) + brush.setAlphaF(0.6) + + painter.setBrush(brush) + + painter.drawRect(self.rect()) + painter.drawText(self.rect(), Qt.AlignCenter, text) + + def process_key(self, key: int) -> None: + if key == Qt.Key_Space and self.status in [ + StatusGameEnum.STARTED, + StatusGameEnum.PAUSED, + ]: + if self.status == StatusGameEnum.STARTED: + self.status = StatusGameEnum.PAUSED + else: + self.status = StatusGameEnum.STARTED + + self.update() + return + + if key in [Qt.Key_Enter, Qt.Key_Return]: + self.start() + return + + if self.board.current_piece and self.status == StatusGameEnum.STARTED: + match key: + case Qt.Key_Left: + self.board.current_piece.move_left() + + case Qt.Key_Right: + self.board.current_piece.move_right() + + case Qt.Key_Up: + self.board.current_piece.turn() + + case Qt.Key_Down: + while self.board.current_piece.move_down(): + pass + + self.update() + return + + def resizeEvent(self, event: QResizeEvent) -> None: + super().resizeEvent(event) + + w_aspect = event.size().width() // self.board.COLS + h_aspect = event.size().height() // self.board.ROWS + + self.cell_size = min(w_aspect, h_aspect) + + self.update() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + self._draw_current_piece(painter) + self._draw_shadow_of_current_piece(painter) + self._draw_board(painter) + self._draw_glass(painter) diff --git a/games/tetris/src/gui/common.py b/games/tetris/src/gui/common.py new file mode 100644 index 000000000..1aee93623 --- /dev/null +++ b/games/tetris/src/gui/common.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import functools +from typing import Callable + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPainter, QColor + + +CELL_SIZE: int = 20 + + +def painter_context(func: Callable): + @functools.wraps(func) + def decorated(*args, **kwargs): + painter: QPainter | None = None + for arg in args + tuple(kwargs.values()): + if isinstance(arg, QPainter): + painter = arg + break + + if painter: + painter.save() + try: + return func(*args, **kwargs) + finally: + if painter: + painter.restore() + + return decorated + + +def draw_cell_board( + painter: QPainter, + x: int, + y: int, + color: QColor, + pen: Qt.GlobalColor = Qt.NoPen, + cell_size: int = CELL_SIZE, + indent: int = 0, +) -> None: + painter.setPen(pen) + painter.setBrush(color) + painter.drawRect( + (x * cell_size) + indent, + (y * cell_size) + indent, + cell_size, + cell_size, + ) + + +class StatusGameEnum(enum.IntEnum): + INITED = enum.auto() + STARTED = enum.auto() + PAUSED = enum.auto() + FINISHED = enum.auto() diff --git a/games/tetris/src/gui/piece_widget.py b/games/tetris/src/gui/piece_widget.py new file mode 100644 index 000000000..e57ada03f --- /dev/null +++ b/games/tetris/src/gui/piece_widget.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + +from PyQt5.QtCore import QSize, Qt +from PyQt5.QtGui import QPaintEvent, QPainter +from PyQt5.QtWidgets import QWidget + +from ..core.piece import Piece +from .common import CELL_SIZE, draw_cell_board + + +class PieceWidget(QWidget): + INDENT: int = 1 + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.piece: Piece | None = None + + def set_piece(self, piece: Piece) -> None: + self.piece = piece + self.update() + + def minimumSizeHint(self) -> QSize: + size = (CELL_SIZE * 4) + (self.INDENT * 2) + return QSize(size, size) + + def paintEvent(self, event: QPaintEvent) -> None: + if not self.piece: + return + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + for x, y in self.piece.get_points_for_state(x=2, y=1): + draw_cell_board(painter, x, y, self.piece.get_color(), pen=Qt.black) diff --git a/games/tic_tac_toe__example.py b/games/tic_tac_toe__example.py index 4afd8c111..85fe6a6a6 100644 --- a/games/tic_tac_toe__example.py +++ b/games/tic_tac_toe__example.py @@ -15,7 +15,7 @@ class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() # Матрица 3 на 3 @@ -26,7 +26,7 @@ def __init__(self): # If True -- X else -- O self._current_figure_flag = True - def _check_win(self): + def _check_win(self) -> None: b = self._board_tic_tac_toe win_X = False win_O = False @@ -80,7 +80,7 @@ def _check_win(self): QMessageBox.information(self, "Winner", "Win O") return - def mouseReleaseEvent(self, e): + def mouseReleaseEvent(self, e) -> None: # Определяем позицию клика i = e.pos().y() // self._size_cell j = e.pos().x() // self._size_cell @@ -97,7 +97,7 @@ def mouseReleaseEvent(self, e): self._check_win() - def paintEvent(self, e): + def paintEvent(self, e) -> None: painter = QPainter(self) painter.setPen(Qt.black) diff --git a/get_actual_news_from_rss_ya/webserver/common.py b/get_actual_news_from_rss_ya/webserver/common.py index 4ec6889cc..6e6bdd67b 100644 --- a/get_actual_news_from_rss_ya/webserver/common.py +++ b/get_actual_news_from_rss_ya/webserver/common.py @@ -24,7 +24,7 @@ def create_connect(): return sqlite3.connect(DB_FILE_NAME) -def init_db(): +def init_db() -> None: # Создание базы и таблицы connect = create_connect() try: @@ -71,10 +71,10 @@ def init_db(): connect.close() -def append_list_news(list_news: [str, str], interest: str): +def append_list_news(list_news: [str, str], interest: str) -> None: connect = create_connect() - def insert_news(title, url, interest): + def insert_news(title, url, interest) -> None: # Для отсеивания дубликатов has = connect.execute("SELECT 1 FROM News WHERE url = ?", (url,)).fetchone() if has: @@ -161,7 +161,7 @@ def get_news_list_and_mark_as_read( connect.close() -def reset_all_is_read(): +def reset_all_is_read() -> None: connect = create_connect() try: diff --git a/get_all_objects_in_memory.py b/get_all_objects_in_memory.py index 42a756ead..ca9390429 100644 --- a/get_all_objects_in_memory.py +++ b/get_all_objects_in_memory.py @@ -14,7 +14,7 @@ # Recursively expand slist's objects # into olist, using seen to track # already processed objects. -def _getr(slist, olist, seen): +def _getr(slist, olist, seen) -> None: for e in slist: if id(e) in seen: continue @@ -47,7 +47,7 @@ def get_all_objects(): print(len(gc.get_objects())) class Foo: - def bar(self): + def bar(self) -> str: return f"Foo(id={hex(id(self))})" items = [Foo() for _ in range(3)] diff --git a/get_cpu_process/get_cpu_process.py b/get_cpu_process/get_cpu_process.py index d498631b1..c03b6151a 100644 --- a/get_cpu_process/get_cpu_process.py +++ b/get_cpu_process/get_cpu_process.py @@ -14,7 +14,7 @@ class ProcessNotFound(Exception): - def __init__(self, name: str): + def __init__(self, name: str) -> None: super().__init__(f"Process {name!r} not found") diff --git a/get_external_ip.py b/get_external_ip.py deleted file mode 100644 index 76081c042..000000000 --- a/get_external_ip.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = "ipetrash" - - -import requests - - -# SOURCE: https://stackoverflow.com/a/54585203/5909792 -def get_external_ip() -> str: - return requests.get("https://www.wikipedia.org").headers["X-Client-IP"] - - -if __name__ == "__main__": - print(get_external_ip()) diff --git a/get_free_port.py b/get_free_port.py index 898259563..112f0f9a4 100644 --- a/get_free_port.py +++ b/get_free_port.py @@ -8,12 +8,9 @@ def get_open_port() -> int: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("", 0)) - port = s.getsockname()[1] - s.close() - - return port + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] if __name__ == "__main__": diff --git a/get_function_caller_name.py b/get_function_caller_name.py index 36bbd295b..52de9833c 100644 --- a/get_function_caller_name.py +++ b/get_function_caller_name.py @@ -14,17 +14,17 @@ def caller_name() -> str: if __name__ == "__main__": - def foo(): + def foo() -> None: print(caller_name()) assert caller_name() == "foo" foo() - def bar(): + def bar() -> None: print(caller_name()) assert caller_name() == "bar" - def zoo(): + def zoo() -> None: print(caller_name()) assert caller_name() == "zoo" diff --git a/get_functions_from_object__module_class_object.py b/get_functions_from_object__module_class_object.py index df7263a0e..38563a639 100644 --- a/get_functions_from_object__module_class_object.py +++ b/get_functions_from_object__module_class_object.py @@ -20,7 +20,7 @@ def get_name_by_func(obj: object) -> dict: print(get_name_by_func(builtins)) class Foo: - def a(self): + def a(self) -> int: return 1 print(get_name_by_func(Foo)) diff --git a/get_md5_file/get_md5_file_gui.py b/get_md5_file/get_md5_file_gui.py index aa1ad0e72..17628e6d5 100644 --- a/get_md5_file/get_md5_file_gui.py +++ b/get_md5_file/get_md5_file_gui.py @@ -45,7 +45,7 @@ from get_md5_file import md5sum -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -61,7 +61,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("get_md5_file_gui") @@ -97,7 +97,7 @@ def __init__(self): self.setCentralWidget(central_widget) - def dragEnterEvent(self, event): + def dragEnterEvent(self, event) -> None: mime = event.mimeData() if mime.hasUrls() and len(mime.urls()) == 1: event.acceptProposedAction() diff --git a/get_my_public_ip/README.md b/get_my_public_ip/README.md deleted file mode 100644 index 8f3f1acea..000000000 --- a/get_my_public_ip/README.md +++ /dev/null @@ -1,4 +0,0 @@ -get_my_public_ip -=========== - -RU: Содержит пример того как можно узнать свой IP. \ No newline at end of file diff --git a/get_my_public_ip/get_my_public_ip__using__ipify_org.py b/get_my_public_ip/api_ipify_org.py similarity index 100% rename from get_my_public_ip/get_my_public_ip__using__ipify_org.py rename to get_my_public_ip/api_ipify_org.py diff --git a/get_my_public_ip/get_my_public_ip__using__api_wipmania_com__with_grab.py b/get_my_public_ip/get_my_public_ip__using__api_wipmania_com__with_grab.py deleted file mode 100644 index 1a99a7c0e..000000000 --- a/get_my_public_ip/get_my_public_ip__using__api_wipmania_com__with_grab.py +++ /dev/null @@ -1,23 +0,0 @@ -__author__ = "ipetrash" - - -""" -Пример того как можно узнать свой ip, -используя запрос на сайт и модуль grab. -""" - - -from grab import Grab - - -def get_my_public_ip() -> str: - g = Grab() - g.setup(proxy="proxy.compassplus.ru:3128", proxy_type="http") - g.go("http://api.wipmania.com") - context = g.doc.body.decode("utf-8") - return context.split("
")[0] - - -if __name__ == "__main__": - ip = get_my_public_ip() - print(f"My IP: {ip}") diff --git a/get_my_public_ip/httpbin_org.py b/get_my_public_ip/httpbin_org.py new file mode 100644 index 000000000..e49ba702e --- /dev/null +++ b/get_my_public_ip/httpbin_org.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +from urllib.request import urlopen + + +def get_my_public_ip() -> str: + with urlopen("https://httpbin.org/get") as f: + return json.loads(f.read())["origin"] + + +if __name__ == "__main__": + ip = get_my_public_ip() + print(f"My IP: {ip}") diff --git a/get_my_public_ip/get_my_public_ip__using__jsonip_com.py b/get_my_public_ip/jsonip_com.py similarity index 100% rename from get_my_public_ip/get_my_public_ip__using__jsonip_com.py rename to get_my_public_ip/jsonip_com.py diff --git a/get_my_public_ip/get_my_public_ip__using__api_wipmania_com.py b/get_my_public_ip/wikipedia_org.py similarity index 67% rename from get_my_public_ip/get_my_public_ip__using__api_wipmania_com.py rename to get_my_public_ip/wikipedia_org.py index aeb9de5f5..9f313336a 100644 --- a/get_my_public_ip/get_my_public_ip__using__api_wipmania_com.py +++ b/get_my_public_ip/wikipedia_org.py @@ -8,8 +8,8 @@ def get_my_public_ip() -> str: - with urlopen("http://api.wipmania.com") as f: - return f.read().decode("utf-8").split("
")[0] + with urlopen("https://www.wikipedia.org") as f: + return f.getheader("X-Client-IP") if __name__ == "__main__": diff --git a/get_quarter.py b/get_quarter.py index fd1f1d718..5e65bdb03 100644 --- a/get_quarter.py +++ b/get_quarter.py @@ -7,7 +7,7 @@ from datetime import date, datetime -def get_quarter(month_or_date=None) -> int: +def get_quarter_num(month_or_date: date | datetime | int | None = None) -> int: dt = month_or_date if dt is None: dt = date.today() @@ -33,44 +33,44 @@ def get_quarter(month_or_date=None) -> int: raise Exception(f'Invalid "month": {month}') -def get_quarter_num(month_or_date=None) -> str: - return ["I", "II", "III", "IV"][get_quarter(month_or_date) - 1] +def get_quarter_roman(month_or_date: date | datetime | int | None = None) -> str: + return ["I", "II", "III", "IV"][get_quarter_num(month_or_date) - 1] if __name__ == "__main__": - print(get_quarter()) - print() - print(get_quarter(datetime.today())) - print(get_quarter(date.today())) - print() print(get_quarter_num()) + print(get_quarter_num(datetime.today())) + print(get_quarter_num(date.today())) + print() + + print(get_quarter_roman()) print() for dt in [date(2018, month=i + 1, day=1) for i in range(12)]: - print(dt, get_quarter(dt)) - - assert get_quarter(1) == 1 - assert get_quarter(2) == 1 - assert get_quarter(3) == 1 - assert get_quarter(4) == 2 - assert get_quarter(5) == 2 - assert get_quarter(6) == 2 - assert get_quarter(7) == 3 - assert get_quarter(8) == 3 - assert get_quarter(9) == 3 - assert get_quarter(10) == 4 - assert get_quarter(11) == 4 - assert get_quarter(12) == 4 - - assert get_quarter_num(1) == "I" - assert get_quarter_num(2) == "I" - assert get_quarter_num(3) == "I" - assert get_quarter_num(4) == "II" - assert get_quarter_num(5) == "II" - assert get_quarter_num(6) == "II" - assert get_quarter_num(7) == "III" - assert get_quarter_num(8) == "III" - assert get_quarter_num(9) == "III" - assert get_quarter_num(10) == "IV" - assert get_quarter_num(11) == "IV" - assert get_quarter_num(12) == "IV" + print(dt, get_quarter_num(dt)) + + assert get_quarter_num(1) == 1 + assert get_quarter_num(2) == 1 + assert get_quarter_num(3) == 1 + assert get_quarter_num(4) == 2 + assert get_quarter_num(5) == 2 + assert get_quarter_num(6) == 2 + assert get_quarter_num(7) == 3 + assert get_quarter_num(8) == 3 + assert get_quarter_num(9) == 3 + assert get_quarter_num(10) == 4 + assert get_quarter_num(11) == 4 + assert get_quarter_num(12) == 4 + + assert get_quarter_roman(1) == "I" + assert get_quarter_roman(2) == "I" + assert get_quarter_roman(3) == "I" + assert get_quarter_roman(4) == "II" + assert get_quarter_roman(5) == "II" + assert get_quarter_roman(6) == "II" + assert get_quarter_roman(7) == "III" + assert get_quarter_roman(8) == "III" + assert get_quarter_roman(9) == "III" + assert get_quarter_roman(10) == "IV" + assert get_quarter_roman(11) == "IV" + assert get_quarter_roman(12) == "IV" diff --git a/github_api__examples/common.py b/github_api__examples/common.py new file mode 100644 index 000000000..b63ea6567 --- /dev/null +++ b/github_api__examples/common.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from pathlib import Path +from dataclasses import dataclass + +import requests + + +@dataclass +class User: + login: str + url: str + + +DIR: Path = Path(__file__).resolve().parent + +TOKEN_FILE_NAME: Path = DIR / "TOKEN.txt" +try: + TOKEN: str = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() +except: + TOKEN: str = "" + + +session = requests.Session() + +if TOKEN: + session.headers["Authorization"] = f"Bearer {TOKEN}" + + +def get_users(url: str) -> list[User]: + per_page: int = 100 + page: int = 1 + + items: list[User] = [] + + while True: + params = dict(per_page=per_page, page=page) + rs = session.get(url, params=params) + rs.raise_for_status() + + result: list[dict] = rs.json() + if not result: + break + + items += [ + User( + login=item["login"], + url=item["html_url"], + ) + for item in result + ] + + page += 1 + + return items diff --git a/github_api__examples/get_followers.py b/github_api__examples/get_followers.py new file mode 100644 index 000000000..68c3b7ad7 --- /dev/null +++ b/github_api__examples/get_followers.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import User, get_users + + +def get_followers(owner: str) -> list[User]: + url: str = f"https://api.github.com/users/{owner}/followers" + return get_users(url) + + +if __name__ == "__main__": + users: list[User] = get_followers("gil9red") + print(f"Followers ({len(users)}):") + print(*users[:5], sep="\n") + print("...") + print(*users[-5:], sep="\n") + """ + Followers (98): + User(login='shakshin', url='https://github.com/shakshin') + User(login='AgelxNash', url='https://github.com/AgelxNash') + User(login='insolor', url='https://github.com/insolor') + User(login='suconakh', url='https://github.com/suconakh') + User(login='oldkiller', url='https://github.com/oldkiller') + ... + User(login='Alexwhite2007', url='https://github.com/Alexwhite2007') + User(login='SPSEBASTIAAN', url='https://github.com/SPSEBASTIAAN') + User(login='ali-lipi', url='https://github.com/ali-lipi') + User(login='Alimkh85', url='https://github.com/Alimkh85') + User(login='barb455555', url='https://github.com/barb455555') + """ diff --git a/github_api__examples/get_last_commit.py b/github_api__examples/get_last_commit.py new file mode 100644 index 000000000..1bf85be62 --- /dev/null +++ b/github_api__examples/get_last_commit.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from datetime import UTC, datetime +from typing import Any + +from common import session + + +@dataclass +class CommitUser: + name: str + email: str + date: datetime + + @classmethod + def parse_from_dict(cls, data: dict[str, Any]) -> "CommitUser": + return cls( + name=data["name"], + email=data["email"], + date=datetime.fromisoformat(data["date"]), + ) + + +# TODO: Поддержка всех полей +@dataclass +class Commit: + sha: str + message: str + author: CommitUser + committer: CommitUser + + @classmethod + def parse_from_dict(cls, data: dict[str, Any]) -> "Commit": + return cls( + sha=data["sha"], + message=data["commit"]["message"], + author=CommitUser.parse_from_dict(data["commit"]["author"]), + committer=CommitUser.parse_from_dict(data["commit"]["committer"]), + ) + + +def get_last_commit(owner: str, repository: str) -> Commit: + url: str = f"https://api.github.com/repos/{owner}/{repository}/commits?per_page=1" + + rs = session.get(url) + rs.raise_for_status() + + result: dict[str, Any] = rs.json()[0] + return Commit.parse_from_dict(result) + + +if __name__ == "__main__": + commit = get_last_commit(owner="gil9red", repository="RPG-Maker-MZ-Foo") + print(commit) + # Commit(sha='816a821788e113fdc80d17190fa1ed743c4b1524', message='Обновление. Улучшение алгоритма шагающей леди', author=CommitUser(name='gil9red', email='ilya.petrash@inbox.ru', date=datetime.datetime(2025, 10, 25, 18, 53, 41, tzinfo=datetime.timezone.utc)), committer=CommitUser(name='gil9red', email='ilya.petrash@inbox.ru', date=datetime.datetime(2025, 10, 25, 18, 53, 41, tzinfo=datetime.timezone.utc))) + + print(datetime.now(UTC) - commit.author.date) + # 2 days, 14:21:39.576065 diff --git a/github_api__examples/get_stargazers.py b/github_api__examples/get_stargazers.py index 7101b9a48..af68c3f97 100644 --- a/github_api__examples/get_stargazers.py +++ b/github_api__examples/get_stargazers.py @@ -4,46 +4,31 @@ __author__ = "ipetrash" -from dataclasses import dataclass -import requests - - -@dataclass -class User: - login: str - url: str +from common import User, get_users def get_stargazers(owner: str, repository: str) -> list[User]: - url = f"https://api.github.com/repos/{owner}/{repository}/stargazers" - - per_page = 100 - page = 1 - - items = [] - - while True: - params = dict(per_page=per_page, page=page) - rs = requests.get(url, params=params) - rs.raise_for_status() - - result: list[dict] = rs.json() - if not result: - break - - items += [ - User( - login=item["login"], - url=item["html_url"], - ) - for item in result - ] - - page += 1 - - return items + url: str = f"https://api.github.com/repos/{owner}/{repository}/stargazers" + return get_users(url) if __name__ == "__main__": - users = get_stargazers("gil9red", "SimplePyScripts") - print(len(users), users) + users: list[User] = get_stargazers("gil9red", "SimplePyScripts") + print(f"Stargazers ({len(users)}):") + print(*users[:5], sep="\n") + print("...") + print(*users[-5:], sep="\n") + """ + Stargazers (153): + User(login='numb7', url='https://github.com/numb7') + User(login='Martin-Winter', url='https://github.com/Martin-Winter') + User(login='Shaar68', url='https://github.com/Shaar68') + User(login='triplekill', url='https://github.com/triplekill') + User(login='JMSwag', url='https://github.com/JMSwag') + ... + User(login='bobophp', url='https://github.com/bobophp') + User(login='emnioj', url='https://github.com/emnioj') + User(login='Wisepal61', url='https://github.com/Wisepal61') + User(login='Codycode91', url='https://github.com/Codycode91') + User(login='jellykioto', url='https://github.com/jellykioto') + """ diff --git a/gitpython_examples/common.py b/gitpython_examples/common.py index ebe0c1da1..49a4e0198 100644 --- a/gitpython_examples/common.py +++ b/gitpython_examples/common.py @@ -31,7 +31,6 @@ def get_repo(): try: return git.Repo(REPO_PATH) - except: return git.Repo.clone_from(URL_GIT, REPO_PATH) @@ -46,7 +45,7 @@ def return_log(reverse=False): return logs -def print_log(reverse=False): +def print_log(reverse=False) -> None: logs = return_log(reverse) print(f"Logs[{len(logs)}]:") diff --git a/gitpython_examples/pull_all_repos.py b/gitpython_examples/pull_all_repos.py index 156cb06d5..db96df41a 100644 --- a/gitpython_examples/pull_all_repos.py +++ b/gitpython_examples/pull_all_repos.py @@ -12,7 +12,7 @@ from common import get_total_commits -def process_repository(repo_path: Path): +def process_repository(repo_path: Path) -> None: print(repo_path) repo = git.Repo(repo_path) @@ -31,7 +31,7 @@ def process_repository(repo_path: Path): print(f"Repository is actual") -def process_all(path: Path, pattern: str = '.git'): +def process_all(path: Path, pattern: str = '.git') -> None: paths: list[Path] = [ p.parent for p in path.rglob(pattern) diff --git a/hex2str/gui.py b/hex2str/gui.py index 002df5707..3ac1a952e 100644 --- a/hex2str/gui.py +++ b/hex2str/gui.py @@ -25,7 +25,7 @@ from hex2str import hex2str, str2hex -def log_uncaught_exceptions(ex_cls, ex, tb): +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: text = f"{ex_cls.__name__}: {ex}:\n" text += "".join(traceback.format_tb(tb)) @@ -38,7 +38,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("hex2str") @@ -124,7 +124,7 @@ def __init__(self): self.setLayout(layout) - def _on_input_selection_changed(self): + def _on_input_selection_changed(self) -> None: try: # Нехорошо будет если второй будет вызывать сигналы, например о выделении текста self.text_edit_output.blockSignals(True) @@ -155,7 +155,7 @@ def _on_input_selection_changed(self): finally: self.text_edit_output.blockSignals(False) - def _on_output_selection_changed(self): + def _on_output_selection_changed(self) -> None: try: # Нехорошо будет если второй будет вызывать сигналы, например о выделении текста self.text_edit_input.blockSignals(True) @@ -186,7 +186,7 @@ def _on_output_selection_changed(self): finally: self.text_edit_input.blockSignals(False) - def show_detail_error_massage(self): + def show_detail_error_massage(self) -> None: message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() @@ -198,7 +198,7 @@ def show_detail_error_massage(self): mb.exec_() - def _convert(self): + def _convert(self) -> None: self.label_error.clear() self.button_detail_error.hide() diff --git a/howlongtobeat__web_wrapper/README.md b/howlongtobeat__web_wrapper/README.md new file mode 100644 index 000000000..807ae943f --- /dev/null +++ b/howlongtobeat__web_wrapper/README.md @@ -0,0 +1,9 @@ +# Example + +http://127.0.0.1:10017/search-game/Half-Life%202 +http://127.0.0.1:10017/docs +http://127.0.0.1:10017/redoc + +``` +python -m uvicorn main:app --port=10017 +``` diff --git a/howlongtobeat__web_wrapper/api_search.py b/howlongtobeat__web_wrapper/api_search.py new file mode 100644 index 000000000..19645100b --- /dev/null +++ b/howlongtobeat__web_wrapper/api_search.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import unicodedata + +from typing import Any + +# NOTE: https://playwright.dev/python/docs/library#pip +# pip install playwright==1.50.0 +# playwright install webkit +from playwright.sync_api import Response, TimeoutError, sync_playwright + +DEFAULT_TIMEOUT_MS: int = 30_000 + + +def _is_found_game(game1: str, game2: str) -> bool: + # SOURCE: https://github.com/gil9red/price_of_games/blob/805f19528c3bcc0999669452c3652698fed22bcd/app_parser/utils.py#L219-L224 + def strip_accents(s: str) -> str: + return "".join( + c + for c in unicodedata.normalize("NFD", s) + if unicodedata.category(c) != "Mn" + ) + + def _process_name(name: str) -> str: + name = strip_accents(name) + return re.sub(r"\W", "", name).lower() + + return _process_name(game1) == _process_name(game2) + + +def get_api_search_raw(game: str) -> dict[str, Any]: + def is_api_search(rs: Response) -> bool: + url: str = rs.url.lower() + return ( + "/api/" in url + and rs.status == 200 + and ("SEARCHTERMS" in str(rs.request.post_data).upper()) + ) + + with sync_playwright() as p: + browser = p.firefox.launch() + + page = browser.new_page() + page.set_default_timeout(DEFAULT_TIMEOUT_MS) + + page.goto(f"https://howlongtobeat.com/?q={game}", wait_until="commit") + + with page.expect_response(is_api_search) as response_info: + return response_info.value.json() + + +def search_game(game: str, max_attempts: int = 5) -> dict[str, Any] | None: + game: str = re.sub("[©®™–]", "", game).replace("`", "'") + + result: dict[str, Any] | None = None + for attempt in range(1, max_attempts + 1): + try: + result = get_api_search_raw(game) + break + except TimeoutError as e: + if attempt >= max_attempts: + raise e + + if not result or "data" not in result: + return + + for obj in result["data"]: + if ( + _is_found_game(game, obj["game_name"]) + # Поиск среди псевдонимов + or obj["game_alias"] == game + or any(_is_found_game(game, name) for name in obj["game_alias"].split(", ")) + ): + return obj + + return + + +if __name__ == "__main__": + game = "Half-Life 2" + result = search_game(game) + print(game, result) + assert game == result["game_name"] + + # game = "Warhammer 40,000: Space Marine 2" + # result = search_game(game) + # print(game, result) + # assert "Warhammer 40,000: Space Marine II" == result["game_name"] + # + # game = "Marc Eckō's Getting Up: Contents Under Pressure" + # result = search_game(game) + # print(game, result) + # assert "Marc Ecko's Getting Up: Contents Under Pressure" == result["game_name"] + # + # game = "Nightmares from the Deep 2: The Siren`s Call" + # result = search_game(game) + # print(game, result) + # assert "Nightmares from the Deep 2: The Siren's Call" == result["game_name"] + # + # print() + # + # game = search_game("Half-Life") + # print(game) + # assert game + # + # game = search_game("Final Fantasy IX") + # print(game) + # assert game + # + # game = search_game("Final Fantasy 9") + # print(game) + # assert game + # + # game = search_game("Final Fantasy VII") + # print(game) + # assert game + + # from multiprocessing.dummy import Pool as ThreadPool + # + # with ThreadPool() as p: + # result = p.map( + # search_game, + # [ + # "dfsdfsdfsdfsdfsdfsdfdsf", + # "Half-Life 2", + # "Half-Life", + # "Final Fantasy", + # "Final Fantasy IX", + # "Final Fantasy 9", + # "Final Fantasy VII", + # ], + # ) + # print(len(result)) + # for obj in result: + # print(obj) diff --git a/howlongtobeat__web_wrapper/main.py b/howlongtobeat__web_wrapper/main.py new file mode 100644 index 000000000..00f6bf92b --- /dev/null +++ b/howlongtobeat__web_wrapper/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Any +from fastapi import FastAPI +from api_search import search_game + + +app = FastAPI() + + +@app.get("/search-game/{name}") +def index(name: str) -> dict[str, Any] | None: + return search_game(name) + + +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=False, + ) diff --git a/html_parsing/rik_i_morti_online.py b/html_parsing/.deprecated/rik_i_morti_online.py similarity index 100% rename from html_parsing/rik_i_morti_online.py rename to html_parsing/.deprecated/rik_i_morti_online.py diff --git a/html_parsing/risens_team__berserk.py b/html_parsing/.deprecated/risens_team__berserk.py similarity index 100% rename from html_parsing/risens_team__berserk.py rename to html_parsing/.deprecated/risens_team__berserk.py diff --git a/html_parsing/yummyanime_club__anime_updates.py b/html_parsing/.deprecated/yummyanime_club__anime_updates.py similarity index 97% rename from html_parsing/yummyanime_club__anime_updates.py rename to html_parsing/.deprecated/yummyanime_club__anime_updates.py index 4a8312d0b..df45253a1 100644 --- a/html_parsing/yummyanime_club__anime_updates.py +++ b/html_parsing/.deprecated/yummyanime_club__anime_updates.py @@ -15,7 +15,7 @@ def get_text(el: Tag) -> str: return text.replace("\xa0", " ") -def parse_update_anime(update_el: Tag): +def parse_update_anime(update_el: Tag) -> None: title = get_text(update_el.select_one(".update-title")) update_date = get_text(update_el.select_one(".update-date")) update_info = get_text(update_el.select_one(".update-info")) diff --git a/html_parsing/yummyanime_club__posts.py b/html_parsing/.deprecated/yummyanime_club__posts.py similarity index 98% rename from html_parsing/yummyanime_club__posts.py rename to html_parsing/.deprecated/yummyanime_club__posts.py index 16573f761..ffdedeb93 100644 --- a/html_parsing/yummyanime_club__posts.py +++ b/html_parsing/.deprecated/yummyanime_club__posts.py @@ -15,7 +15,7 @@ def get_text(el: Tag) -> str: return text.replace("\xa0", " ") -def parse_post_block(post_block: Tag): +def parse_post_block(post_block: Tag) -> None: title_el = post_block.select_one(".post-title") url_post = urljoin(URL, title_el["href"]) title = get_text(title_el) diff --git a/html_parsing/Time_magazine_covers/main.py b/html_parsing/Time_magazine_covers/main.py index f62fddf5c..e5953953a 100644 --- a/html_parsing/Time_magazine_covers/main.py +++ b/html_parsing/Time_magazine_covers/main.py @@ -92,7 +92,7 @@ def dump_covers( out_dir="out", need_zip=False, remove_out_covers=False, -): +) -> None: covers = get_covers(year) out_dir = normpath(abspath(out_dir + f"/{year}")) diff --git a/html_parsing/com_x_life__berserk_chapters.py b/html_parsing/com_x_life__berserk_chapters.py new file mode 100644 index 000000000..dd212762e --- /dev/null +++ b/html_parsing/com_x_life__berserk_chapters.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import re +import time + +from dataclasses import dataclass +from datetime import datetime, date +from typing import Any, Self + +from playwright.sync_api import sync_playwright + + +HOST: str = "https://com-x.life" +URL: str = f"{HOST}/2789-berserk-read-online.html#chapters" + + +@dataclass +class Chapter: + title: str + url: str + pages: int + date: date + + @classmethod + def from_dict(cls, chapter_id: int, d: dict[str, Any]) -> Self: + return cls( + title=d["title"], + url=f"{HOST}/reader/{chapter_id}/{d['id']}", + pages=d["pages"], + date=datetime.strptime(d["date"], "%d.%m.%Y").date(), + ) + + +def get_chapters() -> list[Chapter]: + chapters: list[Chapter] = [] + + with sync_playwright() as p: + browser = p.firefox.launch() + page = browser.new_page() + + page.goto(URL) + + for _ in range(10): + time.sleep(1) + + text: str = page.content() + + m = re.search(r"window.__DATA__\s*=\s*(\{.+?\});", text) + if not m: + continue + + data: dict[str, Any] = json.loads(m.group(1)) + chapter_id: int = data["news_id"] + chapters = [ + Chapter.from_dict(chapter_id, chapter) + for chapter in data["chapters"] + ] + break + + if not chapters: + raise Exception("Не удалось найти главы!") + + return chapters + + +if __name__ == "__main__": + chapters = get_chapters() + print(f"Chapters ({len(chapters)}):") + print(*chapters[:5], sep="\n") + print("...") + print(*chapters[-5:], sep="\n") + """ + Chapters (400): + Chapter(title='Глава 383. Неведение безначально', url='https://com-x.life/reader/2789/741667', pages=24, date=datetime.date(2025, 9, 11)) + Chapter(title='Глава 382. Усыпальница', url='https://com-x.life/reader/2789/719111', pages=22, date=datetime.date(2025, 6, 26)) + Chapter(title='Глава 381. Полумесяц освещающий спину изгнанника', url='https://com-x.life/reader/2789/715388', pages=23, date=datetime.date(2025, 6, 13)) + Chapter(title='Глава 380. Тени умирают дважды.', url='https://com-x.life/reader/2789/662311', pages=20, date=datetime.date(2025, 2, 28)) + Chapter(title='379', url='https://com-x.life/reader/2789/656178', pages=22, date=datetime.date(2025, 2, 14)) + ... + Chapter(title='Том 2. Глава 0D Хранители желаний II', url='https://com-x.life/reader/2789/417510', pages=125, date=datetime.date(2024, 2, 15)) + Chapter(title='Том 1. Глава 0С Хранители желаний I', url='https://com-x.life/reader/2789/417509', pages=60, date=datetime.date(2024, 2, 15)) + Chapter(title='Том 1. Глава 0B Клеймо', url='https://com-x.life/reader/2789/417508', pages=69, date=datetime.date(2024, 2, 15)) + Chapter(title='Том 1. Глава 0А Чёрный Мечник', url='https://com-x.life/reader/2789/417507', pages=96, date=datetime.date(2024, 2, 15)) + Chapter(title='Том 1. Глава 00 Прототип', url='https://com-x.life/reader/2789/417459', pages=46, date=datetime.date(2024, 2, 14)) + """ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/Java. \320\221\320\260\320\267\320\276\320\262\321\213\320\271 \320\272\321\203\321\200\321\201.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/Java. \320\221\320\260\320\267\320\276\320\262\321\213\320\271 \320\272\321\203\321\200\321\201.pdf" index ad3608d34..0d0c9721c 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/Java. \320\221\320\260\320\267\320\276\320\262\321\213\320\271 \320\272\321\203\321\200\321\201.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/Java. \320\221\320\260\320\267\320\276\320\262\321\213\320\271 \320\272\321\203\321\200\321\201.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/JavaScript \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/JavaScript \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" index 014100cf8..0dc67b3ec 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/JavaScript \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/JavaScript \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/PHP - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/PHP - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" new file mode 100644 index 000000000..9a22d26ee Binary files /dev/null and "b/html_parsing/download_my_certificates_from_stepik_org/certs/PHP - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" differ diff --git a/html_parsing/download_my_certificates_from_stepik_org/certs/Python Skills for Handling Biological Data.pdf b/html_parsing/download_my_certificates_from_stepik_org/certs/Python Skills for Handling Biological Data.pdf index dfbf45a88..87416b31b 100644 Binary files a/html_parsing/download_my_certificates_from_stepik_org/certs/Python Skills for Handling Biological Data.pdf and b/html_parsing/download_my_certificates_from_stepik_org/certs/Python Skills for Handling Biological Data.pdf differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/Python \320\276\321\201\320\275\320\276\320\262\321\213 \320\270 \320\277\321\200\320\270\320\274\320\265\320\275\320\265\320\275\320\270\320\265.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/Python \320\276\321\201\320\275\320\276\320\262\321\213 \320\270 \320\277\321\200\320\270\320\274\320\265\320\275\320\265\320\275\320\270\320\265.pdf" index 415b1324d..2d6e30022 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/Python \320\276\321\201\320\275\320\276\320\262\321\213 \320\270 \320\277\321\200\320\270\320\274\320\265\320\275\320\265\320\275\320\270\320\265.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/Python \320\276\321\201\320\275\320\276\320\262\321\213 \320\270 \320\277\321\200\320\270\320\274\320\265\320\275\320\265\320\275\320\270\320\265.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/Python-\321\200\320\260\320\267\321\200\320\260\320\261\320\276\321\202\321\207\320\270\320\272.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/Python-\321\200\320\260\320\267\321\200\320\260\320\261\320\276\321\202\321\207\320\270\320\272.pdf" new file mode 100644 index 000000000..55c0c232e Binary files /dev/null and "b/html_parsing/download_my_certificates_from_stepik_org/certs/Python-\321\200\320\260\320\267\321\200\320\260\320\261\320\276\321\202\321\207\320\270\320\272.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/Ruby - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/Ruby - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" index 89e3d201a..89cd14ef6 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/Ruby - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/Ruby - \320\277\320\265\321\200\320\262\320\276\320\265 \320\267\320\275\320\260\320\272\320\276\320\274\321\201\321\202\320\262\320\276.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\222\320\262\320\265\320\264\320\265\320\275\320\270\320\265 \320\262 SQL.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\222\320\262\320\265\320\264\320\265\320\275\320\270\320\265 \320\262 SQL.pdf" new file mode 100644 index 000000000..7522bbfa8 Binary files /dev/null and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\222\320\262\320\265\320\264\320\265\320\275\320\270\320\265 \320\262 SQL.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\236\321\201\320\275\320\276\320\262\321\213 Git.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\236\321\201\320\275\320\276\320\262\321\213 Git.pdf" index 013f208a8..a7c08a65d 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\236\321\201\320\275\320\276\320\262\321\213 Git.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\236\321\201\320\275\320\276\320\262\321\213 Git.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\265\321\200\320\262\321\213\320\265 \320\277\321\200\320\276\320\263\321\200\320\260\320\274\320\274\321\213 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 Python.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\265\321\200\320\262\321\213\320\265 \320\277\321\200\320\276\320\263\321\200\320\260\320\274\320\274\321\213 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 Python.pdf" new file mode 100644 index 000000000..066cc696a Binary files /dev/null and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\265\321\200\320\262\321\213\320\265 \320\277\321\200\320\276\320\263\321\200\320\260\320\274\320\274\321\213 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 Python.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\276\320\272\320\276\320\273\320\265\320\275\320\270\320\265 Python \320\272\321\203\321\200\321\201 \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\276\320\272\320\276\320\273\320\265\320\275\320\270\320\265 Python \320\272\321\203\321\200\321\201 \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" index 12c1d9ad4..7f019a7df 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\276\320\272\320\276\320\273\320\265\320\275\320\270\320\265 Python \320\272\321\203\321\200\321\201 \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\320\276\320\272\320\276\320\273\320\265\320\275\320\270\320\265 Python \320\272\321\203\321\200\321\201 \320\264\320\273\321\217 \320\275\320\260\321\207\320\270\320\275\320\260\321\216\321\211\320\270\321\205.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Golang.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Golang.pdf" index 46fa6ab98..bcd082ad5 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Golang.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Golang.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Python.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Python.pdf" index 9ef10b282..13e598f86 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Python.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 Python.pdf" differ diff --git "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 C++.pdf" "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 C++.pdf" index d497f9099..8f6dcdbb8 100644 Binary files "a/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 C++.pdf" and "b/html_parsing/download_my_certificates_from_stepik_org/certs/\320\237\321\200\320\276\320\263\321\200\320\260\320\274\320\274\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265 \320\275\320\260 \321\217\320\267\321\213\320\272\320\265 C++.pdf" differ diff --git a/html_parsing/download_my_certificates_from_stepik_org/main.py b/html_parsing/download_my_certificates_from_stepik_org/main.py index 747f99f21..2aad8d4b8 100644 --- a/html_parsing/download_my_certificates_from_stepik_org/main.py +++ b/html_parsing/download_my_certificates_from_stepik_org/main.py @@ -7,6 +7,7 @@ from pathlib import Path from urllib.request import urlretrieve +# pip install pathvalidate from pathvalidate import sanitize_filename from get_certificates import get_certificates @@ -31,6 +32,7 @@ continue print( - f"Downloading certificate file for {course_title!r} course: {file_name.relative_to(DIR)}" + f"Downloading certificate file for {course_title!r} " + f"course: {file_name.relative_to(DIR)}" ) urlretrieve(cert["url"], file_name) diff --git a/html_parsing/encyclopedia_perumov_club__category__dose/db.py b/html_parsing/encyclopedia_perumov_club__category__dose/db.py index b45f1162a..fd822418f 100644 --- a/html_parsing/encyclopedia_perumov_club__category__dose/db.py +++ b/html_parsing/encyclopedia_perumov_club__category__dose/db.py @@ -20,7 +20,7 @@ DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / "database.sqlite") -def db_create_backup(backup_dir="backup"): +def db_create_backup(backup_dir="backup") -> None: os.makedirs(backup_dir, exist_ok=True) file_name = str(dt.datetime.today().date()) + ".sqlite" @@ -43,7 +43,7 @@ class Dossier(BaseModel): url = TextField(unique=True) date = DateField(default=dt.date.today) - def __str__(self): + def __str__(self) -> str: return ( f"{self.__class__.__name__}(id={self.id}, title={self.title!r}, date={self.date}, " f"url={self.url!r}, total_items={len(self.items)})" @@ -55,7 +55,7 @@ class QuestionAnswer(BaseModel): question_text = TextField() answer_text = TextField() - def __str__(self): + def __str__(self) -> str: return ( f"{self.__class__.__name__}(id={self.id}, dossier_id={self.dossier.id}, " f"question_text={shorten(self.question_text)!r}), answer_text={shorten(self.answer_text)!r})" diff --git a/html_parsing/encyclopedia_perumov_club__category__dose/gui.py b/html_parsing/encyclopedia_perumov_club__category__dose/gui.py index 401e0f7a2..e15f56afe 100644 --- a/html_parsing/encyclopedia_perumov_club__category__dose/gui.py +++ b/html_parsing/encyclopedia_perumov_club__category__dose/gui.py @@ -67,7 +67,7 @@ def get_table_widget(header_labels: list) -> QTableWidget: class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.filter_le = QLineEdit() @@ -123,7 +123,7 @@ def _get_search_pattern(self): except: pass - def fill(self): + def fill(self) -> None: # Удаление строк таблицы while self.table_dossier.rowCount(): self.table_dossier.removeRow(0) @@ -155,7 +155,7 @@ def fill(self): self.table_dossier.setCurrentCell(0, 0) self._on_table_dossier_item_clicked() - def _on_table_dossier_item_clicked(self): + def _on_table_dossier_item_clicked(self) -> None: # Удаление строк таблицы while self.table_items.rowCount(): self.table_items.removeRow(0) @@ -199,7 +199,7 @@ def _on_table_dossier_item_clicked(self): self.table_items.setCurrentCell(0, 0) self._on_table_items_item_clicked() - def _on_table_items_item_clicked(self): + def _on_table_items_item_clicked(self) -> None: item = self.table_items.currentItem() if not item: return diff --git a/html_parsing/foxtrot_com_ua__ru__search/main.py b/html_parsing/foxtrot_com_ua__ru__search/main.py index cd7ba0d27..8368e2bf6 100644 --- a/html_parsing/foxtrot_com_ua__ru__search/main.py +++ b/html_parsing/foxtrot_com_ua__ru__search/main.py @@ -78,7 +78,7 @@ def save_goods( 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) diff --git a/html_parsing/get_price_game/from_gama-gama.py b/html_parsing/get_price_game/from_gama-gama.py index ba16194a0..46337d083 100644 --- a/html_parsing/get_price_game/from_gama-gama.py +++ b/html_parsing/get_price_game/from_gama-gama.py @@ -16,7 +16,7 @@ # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() self._page.loadFinished.connect(self._load_finished_handler) @@ -40,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: diff --git a/html_parsing/get_price_game/from_gog.py b/html_parsing/get_price_game/from_gog.py index e6fc4b9ce..08aa0bcb7 100644 --- a/html_parsing/get_price_game/from_gog.py +++ b/html_parsing/get_price_game/from_gog.py @@ -4,25 +4,44 @@ __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" - f"&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 3afb4022c..833aa63e2 100644 --- a/html_parsing/get_price_game/from_origin.py +++ b/html_parsing/get_price_game/from_origin.py @@ -14,7 +14,7 @@ # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() @@ -43,10 +43,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._page.toHtml(self._callable) return ExtractorHtml(url).html diff --git a/html_parsing/get_video_list__xcadr.com.py b/html_parsing/get_video_list__xcadr.com.py index a20ac657c..219ab0db6 100644 --- a/html_parsing/get_video_list__xcadr.com.py +++ b/html_parsing/get_video_list__xcadr.com.py @@ -54,7 +54,7 @@ def get_video_list(url) -> list[dict]: 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( diff --git a/html_parsing/gitmanga_com__get_chapters.py b/html_parsing/gitmanga_com__get_chapters.py index edff54163..002f867dc 100644 --- a/html_parsing/gitmanga_com__get_chapters.py +++ b/html_parsing/gitmanga_com__get_chapters.py @@ -39,7 +39,7 @@ def get_chapters(url: str) -> list[Chapter]: if __name__ == "__main__": - def print_chapter(items: list[Chapter]): + def print_chapter(items: list[Chapter]) -> None: print(f"Chapters ({len(items)}):") print(f" {items[0]}") print(" ...") diff --git a/html_parsing/grouple_co/common.py b/html_parsing/grouple_co/common.py index defc9513a..4af15f367 100644 --- a/html_parsing/grouple_co/common.py +++ b/html_parsing/grouple_co/common.py @@ -10,6 +10,7 @@ import time from dataclasses import dataclass, field +from urllib.parse import urljoin, urlsplit import requests from bs4 import BeautifulSoup, Tag @@ -41,19 +42,51 @@ class Status(AutoName): 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" +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, + "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 @@ -63,11 +96,49 @@ def do_auth() -> requests.Response: return rs -def load(url: str, **kwargs) -> requests.Response: - while True: - rs = session.get(url, **kwargs) - 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) # Если нужно авторизоваться @@ -93,8 +164,7 @@ def parse_bookmark(el: Tag) -> Bookmark: def get_bookmarks_by_status(status: Status) -> list[Bookmark]: - url_bookmarks = "https://grouple.co/private/bookmarks" - rs = load(url_bookmarks) + rs = load("/private/bookmarks") m = re.search(r"var SITES = (\[.+]);", rs.text) if not m: @@ -121,12 +191,9 @@ def get_bookmarks_by_status(status: Status) -> list[Bookmark]: } headers = { "Authorization": f'Bearer {session.cookies["gwt"]}', - "Referer": url_bookmarks, + "Referer": rs.url, } - rs = session.post( - "https://grouple.co/api/bookmark/list", json=data, headers=headers - ) - rs.raise_for_status() + rs = do_post("/api/bookmark/list", json=data, headers=headers) result = rs.json() for item in result["list"]: @@ -145,9 +212,7 @@ def get_bookmarks_by_status(status: Status) -> list[Bookmark]: for el in BeautifulSoup(tags_str, "html.parser").select("span") ] - items.append( - Bookmark(title=title, url=url, tags=tags) - ) + items.append(Bookmark(title=title, url=url, tags=tags)) if result["offset"] + result["limit"] >= result["total"]: break @@ -159,31 +224,31 @@ def get_bookmarks_by_status(status: Status) -> list[Bookmark]: def get_plain_all_bookmarks_from_user(user_id: int) -> list[Bookmark]: - url = f"https://grouple.co/user/{user_id}/bookmarks" - rs = load(url) + 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") - ] + 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 - } + 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("HOST:", HOST) + # HOST: https://1.grouple.co + print("\n" + "-" * 50 + "\n") print(get_plain_all_bookmarks_from_user(315828)) @@ -195,12 +260,18 @@ def get_all_bookmarks() -> dict[Status, list[Bookmark]]: for i, bookmark in enumerate(bookmarks, 1): 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/get_updates_from_api.py b/html_parsing/grouple_co/get_updates_from_api.py index d53c9b693..ce223c428 100644 --- a/html_parsing/grouple_co/get_updates_from_api.py +++ b/html_parsing/grouple_co/get_updates_from_api.py @@ -4,12 +4,12 @@ __author__ = "ipetrash" -from common import session, load +from common import session, do_post, load def get_feeds_by_manga_chapters() -> list[str]: # Auth and load - rs = load("https://grouple.co/private/bookmarks/index#") + rs = load("/private/bookmarks/index#") data = { "bookmarkSort": "NAME", @@ -27,12 +27,11 @@ def get_feeds_by_manga_chapters() -> list[str]: } ) - rs = session.post("https://grouple.co/api/bookmark/activitiesList", json=data) - rs.raise_for_status() - + 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" ] diff --git a/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py index 882bc85b7..34d962d81 100644 --- a/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py +++ b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py @@ -15,7 +15,7 @@ print("\n") -completed = [x for x in items if "переведено" in x.tags] +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}") diff --git a/html_parsing/howlongtobeat_com/common.py b/html_parsing/howlongtobeat_com/common.py index 62f6d2d6f..8c244d9ef 100644 --- a/html_parsing/howlongtobeat_com/common.py +++ b/html_parsing/howlongtobeat_com/common.py @@ -43,7 +43,7 @@ class Game: profile_genres: list[str] = field(default_factory=list) - def __post_init__(self): + 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) @@ -60,8 +60,8 @@ def parse(cls, data: dict[str, Any]) -> "Game": duration_100_seconds=data["comp_100"], duration_all_seconds=data["comp_all"], release_world=data["release_world"], - profile_platforms=data["profile_platform"].split(", "), - # В случаи поиска жанры недоступны в результатах - нужно на страницу игры идти + # В случаи поиска некоторые из полей недоступны в результатах - нужно на страницу игры идти + profile_platforms=data.get("profile_platform", "").split(", "), profile_genres=data.get("profile_genre", "").split(", "), ) diff --git a/html_parsing/howlongtobeat_com/search.py b/html_parsing/howlongtobeat_com/search.py index d10874588..8b4abb967 100644 --- a/html_parsing/howlongtobeat_com/search.py +++ b/html_parsing/howlongtobeat_com/search.py @@ -4,18 +4,97 @@ __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 -URL_SEARCH = f"{URL_BASE}/api/search" +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() -def api_search(text: str, page: int = 1) -> dict[str, Any]: - headers = { - "Referer": f"{URL_BASE}/?q={text}", - } - data = { + headers = {"Referer": url_first} + + # TODO: Перестала быть рабочей. Работа API теперь в файлах вида "/_next/static/chunks/\w+.js" + # # NOTE: Получение url из js. Старая защита. Возможно, будет комбинироваться с новой + # m = re.search(r'