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)}{name}>"
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',) {}
+ TX Version Calendar What you were looking for is just not there. Something unexpected happened... Please try again later Error: {{ e }}
+
+
+
+
+
+38
+
+
+
+
+
+
+
+
+39
+
+
+
+
+
+
+
+
+40
+
+
+
+
+
+
+
+
+
+ 01 02 03 04
+ 05 06 07 08
+ 09 10 11 12
+
+
+2024
+
+
+
+
+
+
+
+
+ 09 10 11 12
+ 01 02 03 04
+ 05 06 07 08
+
+
+2023
+ 2024
+
+
+
+
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 @@
+
+
+
+
+
+
+ 12 01 02 03
+ 04 05 06 07
+ 08 09 10 11
+
+
+
+2022
+ 2023
+
+
+
+ Task
+ Minor Bug
+ Important Bug
+ End of Support ".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
+
+
+ """
+
+
+@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]:
# Рекурсивная функция поиска {{ text }}
+ Page Not Found
+
+
+{% 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
+
+Start page
+
+
+{% 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
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 "
" 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 new file mode 100644 index 000000000..7f8fb28af --- /dev/null +++ b/flask__webservers/users/main_with_db.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.geeksforgeeks.org/how-to-add-authentication-to-your-app-with-flask-login/ + + +import os + +from typing import Optional + +# pip install flask==3.0.0 +import flask + +# pip install flask-login==0.6.2 +import flask_login + +# pip install flask-sqlalchemy==3.1.1 +# TODO: Bug fix for flask-login==0.6.2 - flask-sqlalchemy installed/updated Werkzeug +# pip install Werkzeug==2.3.7 +from flask_sqlalchemy import SQLAlchemy + + +# TODO: Убрать дублирование кода в шаблонах +# TODO: Не хранить пароль в чистом виде +# TODO: Сделать вариант примера с peewee + + +app = flask.Flask(__name__) +app.secret_key = "super secret string" # Change this! +app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI", "sqlite:///db.sqlite") + +db = SQLAlchemy() + +login_manager = flask_login.LoginManager() +login_manager.init_app(app) + + +class User(db.Model, flask_login.UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(250), unique=True, nullable=False) + password = db.Column(db.String(250), nullable=False) + + @classmethod + def get_by(cls, username: str) -> Optional["User"]: + return cls.query.filter_by( + username=username + ).first() + + +db.init_app(app) + +with app.app_context(): + db.create_all() + + +@login_manager.user_loader +def loader_user(user_id: int): + return db.session.get(User, user_id) + + +@app.route("/") +def index(): + return flask.render_template_string(""" + + +
+ + + +
+ + +
+