diff --git a/.env b/.env index 9684b92..c236b7b 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ DB_HOST=10.0.13.3 DB_PORT=5432 -DB_NAME=tools_stock -DB_USER=tools_stock +DB_NAME=toolbox +DB_USER=toolbox DB_PASS=z7kWLkSKa6 \ No newline at end of file diff --git a/config/__pycache__/__init__.cpython-313.pyc b/config/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b1054fe Binary files /dev/null and b/config/__pycache__/__init__.cpython-313.pyc differ diff --git a/db/__init__.py b/db/__init__.py index d1cb303..a292f80 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -108,7 +108,7 @@ class CRUD: item = await db.execute(query) await db.commit() logger.info("Запись обновлена") - return item + return await db.get(db_data, id) except Exception as e: await db.rollback() logger.error(f"Ошибка обновления: {str(e)}", exc_info=True) diff --git a/db/__pycache__/__init__.cpython-313.pyc b/db/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..2cf4a0b Binary files /dev/null and b/db/__pycache__/__init__.cpython-313.pyc differ diff --git a/db/__pycache__/initialize.cpython-313.pyc b/db/__pycache__/initialize.cpython-313.pyc new file mode 100644 index 0000000..77f0e7b Binary files /dev/null and b/db/__pycache__/initialize.cpython-313.pyc differ diff --git a/db/handlers/__init__.py b/db/handlers/__init__.py deleted file mode 100644 index 9fd910d..0000000 --- a/db/handlers/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -from .user import * -from .access import * -from .toolbox import * -from .categories import * -from .stock import * -from .toolkit import * -from .records import * -from .actions import * - - -class InitializeDatabase: - def __init__(self): - self.userHandler = UserHandler() - self.accessHandler = AccessLevelHandler() - self.toolboxHandler = ToolboxHandler() - self.categoryHandler = CategoryHandler() - self.stockHandler = StockHandler() - self.toolkitHandler = ToolkitHandler() - self.stocksRecordHandler = StocksRecordsHandler() - self.servicesRecordHandler = ServiceRecordsHandler() - self.actionsHandler = StocksActions() - - async def initialize(self): - await self.accessHandler.initialize() - await self.userHandler.initialize() - await self.toolboxHandler.initialize() - await self.categoryHandler.initialize() - await self.toolkitHandler.initialize() - await self.actionsHandler.initialize() - - -__all__ = [ - "UserHandler", - "AccessLevelHandler", - "ToolboxHandler", - "CategoryHandler", - "StockHandler", - "ToolkitHandler", - "StocksRecords", - "ServicesRecords", - "StocksRecordsHandler", - "ServiceRecordsHandler", - "StocksActions", - "InitializeDatabase", -] diff --git a/db/handlers/__pycache__/__init__.cpython-313.pyc b/db/handlers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6e56e20 Binary files /dev/null and b/db/handlers/__pycache__/__init__.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/access.cpython-313.pyc b/db/handlers/__pycache__/access.cpython-313.pyc new file mode 100644 index 0000000..b70ed85 Binary files /dev/null and b/db/handlers/__pycache__/access.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/actions.cpython-313.pyc b/db/handlers/__pycache__/actions.cpython-313.pyc new file mode 100644 index 0000000..074fb58 Binary files /dev/null and b/db/handlers/__pycache__/actions.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/categories.cpython-313.pyc b/db/handlers/__pycache__/categories.cpython-313.pyc new file mode 100644 index 0000000..d826a6c Binary files /dev/null and b/db/handlers/__pycache__/categories.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/records.cpython-313.pyc b/db/handlers/__pycache__/records.cpython-313.pyc new file mode 100644 index 0000000..d09f906 Binary files /dev/null and b/db/handlers/__pycache__/records.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/stock.cpython-313.pyc b/db/handlers/__pycache__/stock.cpython-313.pyc new file mode 100644 index 0000000..86d6949 Binary files /dev/null and b/db/handlers/__pycache__/stock.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/toolbox.cpython-313.pyc b/db/handlers/__pycache__/toolbox.cpython-313.pyc new file mode 100644 index 0000000..2ee38f6 Binary files /dev/null and b/db/handlers/__pycache__/toolbox.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/toolkit.cpython-313.pyc b/db/handlers/__pycache__/toolkit.cpython-313.pyc new file mode 100644 index 0000000..87a2ac1 Binary files /dev/null and b/db/handlers/__pycache__/toolkit.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/user.cpython-313.pyc b/db/handlers/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..4875749 Binary files /dev/null and b/db/handlers/__pycache__/user.cpython-313.pyc differ diff --git a/db/handlers/access.py b/db/handlers/access.py index 9744979..037354e 100644 --- a/db/handlers/access.py +++ b/db/handlers/access.py @@ -1,13 +1,13 @@ from sqlalchemy import select from utils import logger from db import CRUD -from db.schemas import AccessLevel -from db.handlers import ServiceRecordsHandler +from db.schemas.access import AccessLevel +from db.handlers.records import ServiceRecordsHandler class AccessLevelHandler: - async def add(**kwargs): - title = kwargs.get("title", None) + async def add(newData): + title = newData.get("title", None) if not title: logger.error("Не указано название уровня доступа") return {} @@ -17,8 +17,8 @@ class AccessLevelHandler: return {} try: logger.info(f"Создание уровня доступа {title}") - user_id = kwargs.pop("user_id", None) - accessData = await AccessLevel(**kwargs).save() + user_id = newData.pop("user_id", None) + accessData = await AccessLevel(**newData).save() await ServiceRecordsHandler.add( user_id, {"Добавлен уровень доступа": accessData.toDict()} ) @@ -85,7 +85,7 @@ class AccessLevelHandler: ) return result - async def initialize(self): + async def initialize(): baseAcessLevels = { "admin": { "title": "Администратор", @@ -149,7 +149,7 @@ class AccessLevelHandler: logger.info("Инициализация уровней доступа") for accessLevel in baseAcessLevels.values(): - await self.add(**accessLevel) + await AccessLevelHandler.add(accessLevel) logger.info("Уровни доступа успешно инициализированы") return diff --git a/db/handlers/actions.py b/db/handlers/actions.py index 0bacf6e..7900fde 100644 --- a/db/handlers/actions.py +++ b/db/handlers/actions.py @@ -1,4 +1,11 @@ -from db.handlers import StockHandler, StocksRecordsHandler +import random +from db.handlers.stock import StockHandler +from db.handlers.toolbox import ToolboxHandler +from db.handlers.toolkit import ToolkitHandler +from db.handlers.user import UserHandler +from db.handlers.access import AccessLevelHandler +from db.handlers.records import StocksRecordsHandler + from utils import logger @@ -32,7 +39,7 @@ class StocksActions: recorded = await StocksRecordsHandler.add( action="Оприходование", source_stock_id=None, - target_stock_id=newStocks.id, + target_stock_id=newStocks.get("id"), source_toolbox_id=None, target_toolbox_id=toolbox_id, toolkit_id=toolkit_id, @@ -46,7 +53,7 @@ class StocksActions: f"Оприходование инструмента {toolkit_id} на складе {toolbox_id} прошло {'успешно' if recorded else 'не успешно'}" ) if recorded: - accepted = await StocksRecordsHandler.accept( + accepted = await StocksRecordsHandler.decide( recorded, user_id, None, quantity, price ) if not accepted: @@ -84,7 +91,7 @@ class StocksActions: return False totalTakeQuantity = 0 - writeDownList = [] + movementsList = [] for stock in availability: if quantity == totalTakeQuantity: @@ -108,14 +115,15 @@ class StocksActions: f"Списание инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} в количестве {takeQuantity} по цене {stock['price']} успешно завершена" ) + movementsList.append( + { + "id": sourceEdit["id"], + "quantity": takeQuantity, + "price": stock["price"], + } + ) + if not target_toolbox_id: - writeDownList.append( - { - "id": sourceEdit["id"], - "quantity": takeQuantity, - "price": stock["price"], - } - ) continue existing = await StockHandler.getByToolboxIdAndToolkitIdAndQPrice( @@ -168,7 +176,6 @@ class StocksActions: reason=reason, quantity=quantity, price=stock["price"], - target_placement=target_placement, ) if not recorded: logger.error( @@ -179,7 +186,7 @@ class StocksActions: logger.info( f"{action} инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} прошло успешно" ) - return True if target_toolbox_id else writeDownList + return movementsList async def movingRequest( action: str, @@ -213,29 +220,50 @@ class StocksActions: ) return recorded - async def movingAcceptance(self, record_id: int, user_id: int): - logger.info(f"Принятие записи о движении инструмента {record_id} ...") - writeDownARecord = await StocksRecordsHandler.getById(record_id, True) - if not writeDownARecord: + async def movingDecision(record_id: int, user_id: int, accepted: bool = True): + logger.info( + f"{'Принятие' if accepted else 'Отклонение'} записи о движении инструмента {record_id} ..." + ) + movingRecord = await StocksRecordsHandler.getById(record_id, True) + if not movingRecord: logger.error(f"Запись {record_id} не найдена") return False - if writeDownARecord.accepted_at is not None: - logger.error(f"Запись {record_id} уже была принята") + if movingRecord.accepted is not None: + logger.error( + f"Запись {record_id} уже была {'принята' if movingRecord.accepted else 'отклонена'}" + ) return False - stocksMovements = await self.moving( - action=writeDownARecord.action, - source_toolbox_id=writeDownARecord.source_toolbox_id, - target_toolbox_id=writeDownARecord.target_toolbox_id, - toolkit_id=writeDownARecord.toolkit_id, - quantity=writeDownARecord.quantity, + if not accepted: + return await StocksRecordsHandler.decide( + record_id, + user_id, + movingRecord.source_stock_id, + movingRecord.quantity, + movingRecord.price, + accepted, + ) + + target_toolbox_id = ( + movingRecord.target_toolbox_id + if movingRecord.action != "Списание" + else None + ) + logger.warning(f"{target_toolbox_id = }, {movingRecord.action = }") + + stocksMovements = await StocksActions.moving( + action=movingRecord.action, + source_toolbox_id=movingRecord.source_toolbox_id, + target_toolbox_id=target_toolbox_id, + toolkit_id=movingRecord.toolkit_id, + quantity=movingRecord.quantity, user_id=user_id, - reason=writeDownARecord.reason, + reason=movingRecord.reason, ) if not stocksMovements: - logger.error(f"Ошибка при {writeDownARecord.action} инструмента") + logger.error(f"Ошибка при {movingRecord.action} инструмента") return False - accept = await StocksRecordsHandler.accept( + accept = await StocksRecordsHandler.decide( record_id, user_id, stocksMovements[0].get("id"), @@ -250,23 +278,23 @@ class StocksActions: if len(stocksMovements) > 1: for stock in stocksMovements[1:]: recorded = await StocksRecordsHandler.add( - action=writeDownARecord.action, + action=movingRecord.action, source_stock_id=stock.get("id"), target_stock_id=None, - source_toolbox_id=writeDownARecord.source_toolbox_id, - target_toolbox_id=writeDownARecord.target_toolbox_id, - toolkit_id=writeDownARecord.toolkit_id, - init_user_id=writeDownARecord.init_user_id, - reason=writeDownARecord.reason, + source_toolbox_id=movingRecord.source_toolbox_id, + target_toolbox_id=target_toolbox_id, + toolkit_id=movingRecord.toolkit_id, + init_user_id=movingRecord.init_user_id, + reason=movingRecord.reason, quantity=stock.get("quantity"), price=stock.get("price"), return_record_id=True, ) if not recorded: return False - accept = await StocksRecordsHandler.accept( + accept = await StocksRecordsHandler.decide( record_id=recorded, - accept_user_id=user_id, + decision_user_id=user_id, source_stock_id=stock.get("id"), quantity=stock.get("quantity"), price=stock.get("price"), @@ -276,12 +304,11 @@ class StocksActions: totalRecordsIds.append(recorded) logger.info( - f"Записи {', '.join(map(str, totalRecordsIds))} о {writeDownARecord.action} инструмента успешно приняты {user_id}" + f"Записи {', '.join(map(str, totalRecordsIds))} о {movingRecord.action} инструмента успешно приняты {user_id}" ) return True async def takeToolkit( - self, source_toolbox_id: int, target_toolbox_id: int, toolkit_id: int, @@ -293,7 +320,7 @@ class StocksActions: logger.info( f"Формирование запроса на получение инструмента {toolkit_id} на склад {target_toolbox_id} со склада {source_toolbox_id} в количестве {quantity} ..." ) - takeRequest = await self.movingRequest( + takeRequest = await StocksActions.movingRequest( action="Получение", source_toolbox_id=source_toolbox_id, target_toolbox_id=target_toolbox_id, @@ -315,7 +342,7 @@ class StocksActions: logger.info( f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} ..." ) - accepted = await self.movingAcceptance(takeRequest, user_id) + accepted = await StocksActions.movingDecision(takeRequest, user_id) if not accepted: logger.error( f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} не удалось" @@ -327,6 +354,226 @@ class StocksActions: return True async def initialize(): - # TODO прописать наполнение общих складов, получение на личные, возвраты и списания. - # Не все запросы на возвраты и списания нужно принять автоматически, нужно оставить несколько для демонстрации - pass + + toolboxes = await ToolboxHandler.getAll() + toolboxesDict = { + toolbox.get("title"): toolbox.get("id") for toolbox in toolboxes + } + toolboxesOwners = { + toolbox.get("owner_id"): toolbox.get("id") + for toolbox in toolboxes + if toolbox.get("owner_id") + } + + users = await UserHandler.getAll() + usersDict = {user.get("login"): user.get("id") for user in users} + + toolkits = await ToolkitHandler.getAll() + + logger.warning("Наполнение складов ...") + for toolkit in toolkits: + if "Сверло" in toolkit.get("title"): + toolboxId = toolboxesDict.get("Шкаф") + else: + toolboxId = toolboxesDict.get("Стеллаж") + + placement = chr(65 + random.randint(0, 25)) + str(random.randint(1, 19)) + + registryCount = 5 + + logger.warning( + f">> Наполнение склада {toolboxId} инструментом {toolkit.get('title')}" + ) + + for i in range(registryCount): + quantity = random.randint(20, 30) + price = round(300 + random.random() * 500, 2) + + logger.warning(f">>>> Количество: {quantity}, Цена: {price}") + + success = await StocksActions.registration( + toolkit_id=toolkit.get("id"), + toolbox_id=toolboxId, + user_id=usersDict.get("storekeeper"), + quantity=quantity, + price=price, + placement=placement, + reason=f"Приход инструмента {toolkit.get('title')}. Счёт-фактура № {random.randint(1000, 10000)}-{i+1}", + ) + if not success: + logger.error(f"Приход инструмента {toolkit.get('title')} не удался") + return False + + logger.warning("Наполнение складов завершено") + + logger.warning("Получение инструментов из общих складов ...") + + accessLevels = await AccessLevelHandler.getAll() + accessLevelsDict = { + accessLevel.get("id"): accessLevel.get("available_own_toolbox") + for accessLevel in accessLevels + } + + users = await UserHandler.getAll() + usersDict = {user.get("login"): user.get("id") for user in users} + + for user in users: + userAccessLevelId = user.get("access_level_id") + if not accessLevelsDict.get(userAccessLevelId): + continue + + own_toolbox_id = toolboxesOwners.get(user.get("id")) + + logger.warning( + f"Получение инструментов из общего склада пользователем {user.get('login')} ..." + ) + + for toolkit in toolkits: + + logger.warning( + f">> Выдача инструмента {toolkit.get('title')} пользователю {user.get('login')} ..." + ) + + if "Сверло" in toolkit.get("title"): + main_toolbox_id = toolboxesDict.get("Шкаф") + else: + main_toolbox_id = toolboxesDict.get("Стеллаж") + + actionsCount = random.randint(3, 5) + + for i in range(actionsCount): + success = await StocksActions.takeToolkit( + source_toolbox_id=main_toolbox_id, + target_toolbox_id=own_toolbox_id, + toolkit_id=toolkit.get("id"), + quantity=random.randint(10, 19), + reason=f"Получение инструмента {toolkit.get('title')}. Для выполнения заказа № {random.randint(1000, 10000)}", + user_id=user.get("id"), + ) + if not success: + logger.error( + f"Получение инструмента {toolkit.get('title')} пользователю {user.get('login')} не удалось" + ) + return False + + logger.warning( + f">>>> Получение инструмента {toolkit.get('title')} пользователю {user.get('login')} успешно завершено" + ) + + logger.warning("Получение инструментов из общих складов завершено") + + logger.warning("Направление запросов на возврат и списание инструментов ...") + + requestsDict = {"Списание": [], "Возврат": []} + + for user in users: + userAccessLevelId = user.get("access_level_id") + if not accessLevelsDict.get(userAccessLevelId): + continue + + own_toolbox_id = toolboxesOwners.get(user.get("id")) + + logger.warning( + f"Направление запросов на возврат и списание инструмента от пользователя {user.get('login')} ..." + ) + + for action in requestsDict.keys(): + for toolkit in toolkits: + if action == "Списание": + main_toolbox_id = None + else: + if "Сверло" in toolkit.get("title"): + main_toolbox_id = toolboxesDict.get("Шкаф") + else: + main_toolbox_id = toolboxesDict.get("Стеллаж") + + actionsCount = random.randint(1, 3) + + for i in range(actionsCount): + success = await StocksActions.movingRequest( + action=action, + toolkit_id=toolkit.get("id"), + source_toolbox_id=own_toolbox_id, + target_toolbox_id=main_toolbox_id, + quantity=random.randint(3, 5), + reason=f"{action} инструмента {toolkit.get('title')}. После выполнения заказа", + user_id=user.get("id"), + return_record_id=True, + ) + if not success: + logger.error( + f"{action} инструмента {toolkit.get('title')} пользователю {user.get('login')} не удалось" + ) + return False + + requestsDict.get(action).append(success) + + logger.warning( + f">>>> {action} инструмента пользователю {user.get('login')} успешно завершено" + ) + + logger.warning( + "Направление запросов на возврат и списание инструментов завершено" + ) + + logger.warning("Обработка запросов на возврат и списание инструментов ...") + + decisionsDict = { + "manager": {"accept": [], "reject": [], "ignore": []}, + "storekeeper": {"accept": [], "reject": [], "ignore": []}, + } + for recordsList in requestsDict.values(): + managerList, storekeeperList = ( + recordsList[: len(recordsList) // 2], + recordsList[len(recordsList) // 2 :], + ) + + ( + decisionsDict["manager"]["accept"], + decisionsDict["manager"]["reject"], + decisionsDict["manager"]["ignore"], + ) = ( + managerList[: len(managerList) // 3], + managerList[len(managerList) // 3 : len(managerList) // 3 * 2], + managerList[len(managerList) // 3 * 2 :], + ) + + ( + decisionsDict["storekeeper"]["accept"], + decisionsDict["storekeeper"]["reject"], + decisionsDict["storekeeper"]["ignore"], + ) = ( + storekeeperList[: len(storekeeperList) // 3], + storekeeperList[ + len(storekeeperList) // 3 : len(storekeeperList) // 3 * 2 + ], + storekeeperList[len(storekeeperList) // 3 * 2 :], + ) + + for role in decisionsDict.keys(): + user_id = usersDict.get(role) + for decision in decisionsDict.get(role).keys(): + match decision: + case "accept": + accepted = True + case "reject": + accepted = False + case "ignore": + continue + for record_id in decisionsDict.get(role).get(decision): + success = await StocksActions.movingDecision( + record_id=record_id, + user_id=user_id, + accepted=accepted, + ) + if not success: + logger.error( + f"Принятие записи {record_id} пользователем {role} не удалось" + ) + return False + + logger.warning( + "Обработка запросов на возврат и списание инструментов завершено" + ) + + return True diff --git a/db/handlers/categories.py b/db/handlers/categories.py index ed6cb25..45d1c9d 100644 --- a/db/handlers/categories.py +++ b/db/handlers/categories.py @@ -1,8 +1,8 @@ from sqlalchemy import select from utils import logger from db import CRUD -from db.schemas import Category -from db.handlers import ServiceRecordsHandler +from db.schemas.categories import Category +from db.handlers.records import ServiceRecordsHandler class CategoryHandler: diff --git a/db/handlers/records.py b/db/handlers/records.py index 1d5948f..d17ae2a 100644 --- a/db/handlers/records.py +++ b/db/handlers/records.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta from sqlalchemy import select -from db.schemas import StocksRecords, ServicesRecords +from db.handlers.stock import StockHandler +from db.schemas.records import StocksRecords, ServicesRecords from utils import logger @@ -44,28 +45,37 @@ class StocksRecordsHandler: logger.error(f"Ошибка создания записи: {str(e)}") return False - async def accept( + async def decide( record_id: int, - accept_user_id: int, + decision_user_id: int, source_stock_id: int, quantity: int, price: float, + accept: bool = True, ): try: - logger.info(f"Принятие записи {record_id} от {accept_user_id}") - record = await StocksRecords.get(id=record_id) - record.accept_user_id = accept_user_id - record.accepted_at = datetime.now() + logger.info( + f"{'Принятие' if accept else 'Отклонение'} записи {record_id} от {decision_user_id}" + ) + record = await StocksRecordsHandler.getById(record_id, record=True) + if not record: + logger.error(f"Запись {record_id} не найдена") + return False + record.decision_user_id = decision_user_id + record.decided_at = datetime.now() record.source_stock_id = source_stock_id record.quantity = quantity record.price = price + record.accepted = accept await record.save() logger.info( - f"Запись {record_id} успешно принята {accept_user_id} в {record.accepted_at.strftime('%Y-%m-%d %H:%M:%S')}" + f"Запись {record_id} успешно {'принята' if accept else 'отклонена'} {decision_user_id} в {record.decided_at.strftime('%Y-%m-%d %H:%M:%S')}" ) return True except Exception as e: - logger.error(f"Ошибка принятия записи: {str(e)}") + logger.error( + f"Ошибка {'принятия' if accept else 'отклонения'} записи: {str(e)}" + ) return False async def edit(record_id: int, edit_user_id: int, **kwargs): @@ -77,8 +87,6 @@ class StocksRecordsHandler: return recordDB try: - from db.handlers.stock import StockHandler - logger.info(f"Обновление записи {record_id} от {edit_user_id}") record = await StocksRecords.get(id=record_id) diff --git a/db/handlers/stock.py b/db/handlers/stock.py index 6c874a4..3463ccc 100644 --- a/db/handlers/stock.py +++ b/db/handlers/stock.py @@ -1,5 +1,5 @@ from sqlalchemy import select -from db.schemas import Stock +from db.schemas.stock import Stock from utils import logger @@ -45,7 +45,7 @@ class StockHandler: stock = await CRUD.read(select(Stock).where(Stock.id == stockId)) if not stock: - logger.error("Запись об остатках не найдена") + logger.error(f"Запись {stockId} об остатках не найдена") return {} return filterQuantity(stock, filtered) if not record else stock diff --git a/db/handlers/toolbox.py b/db/handlers/toolbox.py index c78e834..21b8d75 100644 --- a/db/handlers/toolbox.py +++ b/db/handlers/toolbox.py @@ -1,8 +1,8 @@ from utils import logger from db import CRUD -from db.schemas import Toolbox +from db.schemas.toolbox import Toolbox from sqlalchemy import or_, select -from db.handlers import ServiceRecordsHandler +from db.handlers.records import ServiceRecordsHandler class ToolboxHandler: diff --git a/db/handlers/toolkit.py b/db/handlers/toolkit.py index d6a8549..bf550d5 100644 --- a/db/handlers/toolkit.py +++ b/db/handlers/toolkit.py @@ -1,8 +1,8 @@ from utils import logger, saveImage, safeFilename, deleteImage from db import CRUD -from db.schemas import Toolkit +from db.schemas.toolkit import Toolkit from sqlalchemy import select -from db.handlers import ServiceRecordsHandler +from db.handlers.records import ServiceRecordsHandler def handleToolkitImage(imageData, title: str): @@ -51,10 +51,8 @@ class ToolkitHandler: return {} logger.info(f"Инструмент {newToolkit.title} успешно создан") - await ServiceRecordsHandler.add( - user_id, {"Добавлен инструмент": toolkitData.toDict()} - ) - return newToolkit.toDict() + await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData}) + return newToolkit async def edit(toolkitId: int, **kwargs): query = select(Toolkit).where(Toolkit.id == toolkitId) @@ -155,7 +153,7 @@ class ToolkitHandler: ) return result - async def initialize(self): + async def initialize(): from .categories import CategoryHandler logger.info("Инициализация инструментов") @@ -203,7 +201,7 @@ class ToolkitHandler: "external_link": "https://nazv.ru", }, { - "title": "Псластина №1", + "title": "Пластина №1", "description": "Пластина такая сякая этакая #1", "specifications": { "Размер": "10", @@ -216,7 +214,7 @@ class ToolkitHandler: "external_link": "https://nazv.ru", }, { - "title": "Псластина №2", + "title": "Пластина №2", "description": "Пластина такая сякая этакая #2", "specifications": { "Размер": "10", @@ -229,7 +227,7 @@ class ToolkitHandler: "external_link": "https://nazv.ru", }, { - "title": "Псластина №3", + "title": "Пластина №3", "description": "Пластина такая сякая этакая #3", "specifications": { "Размер": "10", @@ -283,7 +281,7 @@ class ToolkitHandler: ] for toolkit in baseToolkits: - await self.add(toolkit) + await ToolkitHandler.add(toolkit) logger.info("Базовые инструменты успешно созданы") return diff --git a/db/handlers/user.py b/db/handlers/user.py index 4955875..ce2e82d 100644 --- a/db/handlers/user.py +++ b/db/handlers/user.py @@ -1,9 +1,10 @@ from sqlalchemy import or_, select from db import CRUD -from db.handlers.toolbox import addNewToolbox +from db.handlers.access import AccessLevelHandler +from db.handlers.toolbox import ToolboxHandler from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify -from db.schemas import User -from db.handlers import ServiceRecordsHandler +from db.schemas.user import User +from db.handlers.records import ServiceRecordsHandler def handleUserPhoto(imageData, login: str): @@ -52,20 +53,26 @@ class UserHandler: logger.info( f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}" ) - if newUser.available_own_toolbox: + userAccessLevel = await AccessLevelHandler.get(newUser.access_level_id) + if not userAccessLevel: + logger.error("Уровень доступа не найден") + return {} + if userAccessLevel.get("available_own_toolbox"): newToolboxData = { "title": f"Тулбокс {newUser.username}", "description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность", "owner_id": newUser.id, } - newToolbox = await addNewToolbox(newToolboxData) + newToolbox = await ToolboxHandler.add(newToolboxData) logger.info( f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}" ) await ServiceRecordsHandler.add( user_id, {"Добавлен пользователь": newUser.toDict()} ) - return newUser.toDict() + newUserData = newUser.toDict() + newUserData["access_level_data"] = userAccessLevel + return newUserData async def edit(userData: dict, user_id: int = None) -> dict: id = userData.get("id", None) @@ -107,7 +114,7 @@ class UserHandler: "description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность", "owner_id": editedUser.id, } - newToolbox = await addNewToolbox(newToolboxData) + newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData) logger.info( f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" ) @@ -182,7 +189,7 @@ class UserHandler: ) return userData - async def initialize(self): + async def initialize(): from .access import AccessLevelHandler logger.info("Инициализация пользователей") @@ -190,34 +197,36 @@ class UserHandler: acessLevels = { accessLevel["title"]: accessLevel["id"] for accessLevel in accessLevelsList } + logger.info(acessLevels) + password = "Alex0172" baseUsers = { "admin": { "login": "admin", - "username": "Администратор", - "password": "Alex0172", + "username": "Администратор - Demo", + "password": password, "access_level_id": acessLevels["Администратор"], }, "manager": { "login": "manager", - "username": "Менеджер", - "password": "Alex0172", + "username": "Менеджер - Demo", + "password": password, "access_level_id": acessLevels["Менеджер"], }, "storekeeper": { "login": "storekeeper", - "username": "Кладовщик", - "password": "Alex0172", + "username": "Кладовщик - Demo", + "password": password, "access_level_id": acessLevels["Кладовщик"], }, "employee": { "login": "employee", - "username": "Сотрудник", - "password": "Alex0172", + "username": "Сотрудник - Demo", + "password": password, "access_level_id": acessLevels["Сотрудник"], }, } for user in baseUsers.values(): - await self.add(user) + await UserHandler.add(user) logger.info("Инициализация пользователей завершена") return diff --git a/db/initialize.py b/db/initialize.py new file mode 100644 index 0000000..065ee9d --- /dev/null +++ b/db/initialize.py @@ -0,0 +1,153 @@ +from db.handlers.access import AccessLevelHandler +from db.handlers.user import UserHandler +from db.handlers.toolbox import ToolboxHandler +from db.handlers.categories import CategoryHandler +from db.handlers.toolkit import ToolkitHandler +from db.handlers.actions import StocksActions + +from typing import Optional +from sqlalchemy import inspect, text +from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine +from sqlalchemy.schema import CreateTable +from utils import logger + + +class DatabaseInitializer: + existing_tables: Optional[list[str]] = None + + def __init__(self, database_url: str): + from db import Base + + self.database_url = database_url + self.engine: Optional[AsyncEngine] = None + self.metadata = Base.metadata + + async def initialize(self, force: bool = False, reNewDB: bool = False): + """Main database initialization method""" + try: + self.engine = create_async_engine(self.database_url) + + async with self.engine.begin() as conn: + if force: + logger.info("Принудительное удаление и создание баз...") + await self._drop_all() + await self._create_tables_directly() + await self._initialize_data(reNewDB) + elif not await self._check_tables_exist(conn): + logger.info("Не все необходимые таблицы существуют. Создаем...") + await self._create_tables_directly() + + # Проверяем после создания + async with self.engine.begin() as conn: + if not await self._check_tables_exist(conn): + raise RuntimeError("Не все необходимые таблицы существуют!") + + if reNewDB: + logger.info("Принудительная загрузка данных...") + await self._initialize_data(reNewDB) + else: + logger.info("Все необходимые таблицы существуют. Пропускаем...") + + except Exception as e: + logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}") + raise + finally: + if self.engine: + await self.engine.dispose() + + async def _check_tables_exist(self, conn) -> bool: + """Check if all tables from metadata exist""" + try: + DatabaseInitializer.existing_tables = await conn.run_sync( + lambda sync_conn: inspect(sync_conn).get_table_names() + ) + + required_tables = set(self.metadata.tables.keys()) + + if not required_tables: + logger.error("Нет данных о таблицах в метаданных") + return False + + missing_tables = required_tables - set(DatabaseInitializer.existing_tables) + if missing_tables: + logger.warning("Существующие таблицы:") + logger.info(DatabaseInitializer.existing_tables) + logger.warning("Необходимые таблицы:") + logger.info(required_tables) + logger.warning("Отсутствующие таблицы:") + logger.info(missing_tables) + return False + + return True + except Exception as e: + logger.warning(f"Проверка таблиц завершилась ошибкой: {str(e)}") + return False + + async def _create_tables_directly(self): + """Create tables directly using SQLAlchemy (bypass Alembic)""" + async with self.engine.begin() as conn: + # Создаем все таблицы из метаданных + for table in self.metadata.sorted_tables: + try: + if ( + DatabaseInitializer.existing_tables + and table.name in DatabaseInitializer.existing_tables + ): + logger.debug( + f"Таблица {table.name} уже существует. Пропускаем..." + ) + continue + create_stmt = CreateTable(table) + logger.info(f"Создаем таблицу: {table.name}") + await conn.execute(create_stmt) + except Exception as e: + logger.error(f"Ошибка создания таблицы: {str(e)}") + logger.warning("Все таблицы успешно созданы") + + async def _drop_all(self): + """Drop all tables""" + async with self.engine.begin() as conn: + # Получаем список всех таблиц + existing_tables = await conn.run_sync( + lambda sync_conn: inspect(sync_conn).get_table_names() + ) + + for table in existing_tables: + drop_stmt = text( + f'DROP TABLE "{table}" CASCADE' + ) # Кавычки на случай спец. символов + logger.info(f"Удаляем таблицу: {table}") + await conn.execute(drop_stmt) + + logger.warning("Все таблицы успешно удалены") + + async def _initialize_data(self, waiting: bool = False): + """Initialize required data""" + + def waitForUser(waiting): + if waiting: + input("Для продолжения нажмите Enter...") + + try: + logger.info("Инициализация данных...") + logger.warning("Инициализация Прав доступа...") + await AccessLevelHandler.initialize() + # waitForUser(waiting) + logger.warning("Инициализация Пользователей...") + await UserHandler.initialize() + # waitForUser(waiting) + logger.warning("Инициализация Туллбоксов...") + await ToolboxHandler.initialize() + # waitForUser(waiting) + logger.warning("Инициализация Категорий...") + await CategoryHandler.initialize() + # waitForUser(waiting) + logger.warning("Инициализация Инструментов...") + await ToolkitHandler.initialize() + # waitForUser(waiting) + logger.warning("Инициализация Складов...") + await StocksActions.initialize() + logger.info("Данные успешно инициализированы") + except Exception as e: + logger.error(f"Инициализация данных завершилась ошибкой: {str(e)}") + raise diff --git a/db/schemas/__init__.py b/db/schemas/__init__.py index 314ee01..adefaaf 100644 --- a/db/schemas/__init__.py +++ b/db/schemas/__init__.py @@ -5,134 +5,6 @@ from .categories import * from .toolbox import * from .stock import * -from typing import Optional -from sqlalchemy import inspect, text -from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine -from sqlalchemy.schema import CreateTable -from utils import logger - -# Импортируем все модели -from db import Base -from db.handlers import InitializeDatabase - - -class DatabaseInitializer: - existing_tables: Optional[list[str]] = None - - def __init__(self, database_url: str): - self.database_url = database_url - self.engine: Optional[AsyncEngine] = None - self.metadata = Base.metadata - - async def initialize(self, force: bool = False, reNewDB: bool = False): - """Main database initialization method""" - try: - self.engine = create_async_engine(self.database_url) - - async with self.engine.begin() as conn: - if force: - logger.info("Принудительное удаление и создание баз...") - await self._drop_all() - await self._create_tables_directly() - await self._initialize_data() - elif not await self._check_tables_exist(conn): - logger.info("Не все необходимые таблицы существуют. Создаем...") - await self._create_tables_directly() - - # Проверяем после создания - async with self.engine.begin() as conn: - if not await self._check_tables_exist(conn): - raise RuntimeError("Не все необходимые таблицы существуют!") - - if reNewDB: - logger.info("Принудительная загрузка данных...") - await self._initialize_data() - else: - logger.info("Все необходимые таблицы существуют. Пропускаем...") - - except Exception as e: - logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}") - raise - finally: - if self.engine: - await self.engine.dispose() - - async def _check_tables_exist(self, conn) -> bool: - """Check if all tables from metadata exist""" - try: - DatabaseInitializer.existing_tables = await conn.run_sync( - lambda sync_conn: inspect(sync_conn).get_table_names() - ) - - required_tables = set(self.metadata.tables.keys()) - - if not required_tables: - logger.error("Нет данных о таблицах в метаданных") - return False - - missing_tables = required_tables - set(DatabaseInitializer.existing_tables) - if missing_tables: - logger.warning("Существующие таблицы:") - logger.info(DatabaseInitializer.existing_tables) - logger.warning("Необходимые таблицы:") - logger.info(required_tables) - logger.warning("Отсутствующие таблицы:") - logger.info(missing_tables) - return False - - return True - except Exception as e: - logger.warning(f"Проверка таблиц завершилась ошибкой: {str(e)}") - return False - - async def _create_tables_directly(self): - """Create tables directly using SQLAlchemy (bypass Alembic)""" - async with self.engine.begin() as conn: - # Создаем все таблицы из метаданных - for table in self.metadata.sorted_tables: - try: - if ( - DatabaseInitializer.existing_tables - and table.name in DatabaseInitializer.existing_tables - ): - logger.debug( - f"Таблица {table.name} уже существует. Пропускаем..." - ) - continue - create_stmt = CreateTable(table) - logger.info(f"Создаем таблицу: {table.name}") - await conn.execute(create_stmt) - except Exception as e: - logger.error(f"Ошибка создания таблицы: {str(e)}") - logger.warning("Все таблицы успешно созданы") - - async def _drop_all(self): - """Drop all tables""" - async with self.engine.begin() as conn: - # Получаем список всех таблиц - existing_tables = await conn.run_sync( - lambda sync_conn: inspect(sync_conn).get_table_names() - ) - - for table in existing_tables: - drop_stmt = text( - f'DROP TABLE "{table}" CASCADE' - ) # Кавычки на случай спец. символов - logger.info(f"Удаляем таблицу: {table}") - await conn.execute(drop_stmt) - - logger.warning("Все таблицы успешно удалены") - - async def _initialize_data(self): - """Initialize required data""" - try: - logger.info("Инициализация данных...") - await InitializeDatabase.initialize() - logger.info("Данные успешно инициализированы") - except Exception as e: - logger.error(f"Инициализация данных завершилась ошибкой: {str(e)}") - raise - __all__ = [ "User", @@ -141,4 +13,4 @@ __all__ = [ "Category", "Stock", "Toolkit", -] \ No newline at end of file +] diff --git a/db/schemas/__pycache__/__init__.cpython-313.pyc b/db/schemas/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..651e7d6 Binary files /dev/null and b/db/schemas/__pycache__/__init__.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/access.cpython-313.pyc b/db/schemas/__pycache__/access.cpython-313.pyc new file mode 100644 index 0000000..6b44e0c Binary files /dev/null and b/db/schemas/__pycache__/access.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/categories.cpython-313.pyc b/db/schemas/__pycache__/categories.cpython-313.pyc new file mode 100644 index 0000000..cd79ed7 Binary files /dev/null and b/db/schemas/__pycache__/categories.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/records.cpython-313.pyc b/db/schemas/__pycache__/records.cpython-313.pyc new file mode 100644 index 0000000..fdfd98a Binary files /dev/null and b/db/schemas/__pycache__/records.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/stock.cpython-313.pyc b/db/schemas/__pycache__/stock.cpython-313.pyc new file mode 100644 index 0000000..4eda2c8 Binary files /dev/null and b/db/schemas/__pycache__/stock.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/toolbox.cpython-313.pyc b/db/schemas/__pycache__/toolbox.cpython-313.pyc new file mode 100644 index 0000000..f290ce4 Binary files /dev/null and b/db/schemas/__pycache__/toolbox.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/toolkit.cpython-313.pyc b/db/schemas/__pycache__/toolkit.cpython-313.pyc new file mode 100644 index 0000000..d0eeef1 Binary files /dev/null and b/db/schemas/__pycache__/toolkit.cpython-313.pyc differ diff --git a/db/schemas/__pycache__/user.cpython-313.pyc b/db/schemas/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..9a3ef58 Binary files /dev/null and b/db/schemas/__pycache__/user.cpython-313.pyc differ diff --git a/db/schemas/access.py b/db/schemas/access.py index 7004bc8..a281ef6 100644 --- a/db/schemas/access.py +++ b/db/schemas/access.py @@ -1,6 +1,6 @@ from datetime import datetime from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text -from db import Base +from db import Base, CRUD import utils @@ -44,11 +44,7 @@ class AccessLevel(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(self, **kwargs): - from db import CRUD - return await CRUD.update(AccessLevel, self.id, **kwargs) diff --git a/db/schemas/categories.py b/db/schemas/categories.py index bef42b9..2c00b14 100644 --- a/db/schemas/categories.py +++ b/db/schemas/categories.py @@ -1,6 +1,6 @@ from datetime import datetime from sqlalchemy import Column, DateTime, Integer, String, Text -from db import Base +from db import Base, CRUD import utils @@ -21,11 +21,7 @@ class Category(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(id: int, **kwargs): - from db import CRUD - return await CRUD.update(Category, id, **kwargs) diff --git a/db/schemas/records.py b/db/schemas/records.py index b84b19b..d56cb6f 100644 --- a/db/schemas/records.py +++ b/db/schemas/records.py @@ -1,7 +1,16 @@ from datetime import datetime -from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String, Text +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Integer, + String, + Text, +) from sqlalchemy.dialects.postgresql import JSONB -from db import Base +from db import Base, CRUD import utils @@ -28,18 +37,19 @@ class StocksRecords(Base): init_user_id = Column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) - accept_user_id = Column( + decision_user_id = Column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True ) reason = Column(Text, nullable=False) quantity = Column(Integer, nullable=False) price = Column(Float, nullable=False) + accepted = Column(Boolean, default=None, nullable=True) edit_user_id = Column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True ) edited = Column(JSONB, nullable=True) created_at = Column(DateTime, default=datetime.now) - accepted_at = Column(DateTime, nullable=True) + decided_at = Column(DateTime, nullable=True) edited_at = Column(DateTime, nullable=True) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) @@ -51,13 +61,9 @@ class StocksRecords(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(self, **kwargs): - from db import CRUD - return await CRUD.update(StocksRecords, self.id, **kwargs) @@ -77,6 +83,4 @@ class ServicesRecords(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) diff --git a/db/schemas/stock.py b/db/schemas/stock.py index 6ccdf81..cc980b1 100644 --- a/db/schemas/stock.py +++ b/db/schemas/stock.py @@ -2,7 +2,7 @@ from datetime import datetime from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String from sqlalchemy.orm import relationship -from db import Base +from db import Base, CRUD import utils @@ -14,13 +14,21 @@ class Stock(Base): Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False ) toolkit_data = relationship( - "Toolkit", cascade="all, delete-orphan", lazy="joined", uselist=False + "Toolkit", + cascade="all, delete-orphan", + lazy="joined", + uselist=False, + single_parent=True, ) toolbox_id = Column( Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False ) toolbox_data = relationship( - "Toolbox", cascade="all, delete-orphan", lazy="joined", uselist=False + "Toolbox", + cascade="all, delete-orphan", + lazy="joined", + uselist=False, + single_parent=True, ) quantity = Column(Integer, nullable=False) price = Column(Float, nullable=False) @@ -36,11 +44,7 @@ class Stock(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(self, **kwargs): - from db import CRUD - return await CRUD.update(Stock, self.id, **kwargs) diff --git a/db/schemas/toolbox.py b/db/schemas/toolbox.py index 14a1a17..9520d1b 100644 --- a/db/schemas/toolbox.py +++ b/db/schemas/toolbox.py @@ -1,6 +1,6 @@ from datetime import datetime from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text -from db import Base +from db import Base, CRUD import utils @@ -25,11 +25,7 @@ class Toolbox(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(id: int, **kwargs): - from db import CRUD - return await CRUD.update(Toolbox, id, **kwargs) diff --git a/db/schemas/toolkit.py b/db/schemas/toolkit.py index ebf4c22..7396fce 100644 --- a/db/schemas/toolkit.py +++ b/db/schemas/toolkit.py @@ -2,7 +2,7 @@ from datetime import datetime from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship -from db import Base +from db import Base, CRUD import utils @@ -15,7 +15,11 @@ class Toolkit(Base): specifications = Column(JSONB, default={}) category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE")) category_data = relationship( - "Category", cascade="all, delete-orphan", lazy="joined", uselist=False + "Category", + cascade="all, delete-orphan", + lazy="joined", + uselist=False, + single_parent=True, ) image = Column(JSONB) quantity_min = Column(Integer, nullable=True) @@ -34,11 +38,7 @@ class Toolkit(Base): return utils.toDict(self) async def save(self): - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(id: int, **kwargs): - from db import CRUD - return await CRUD.update(Toolkit, id, **kwargs) diff --git a/db/schemas/user.py b/db/schemas/user.py index ee7203b..d5c9f0f 100644 --- a/db/schemas/user.py +++ b/db/schemas/user.py @@ -1,6 +1,6 @@ from datetime import datetime from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String -from db import Base +from db import Base, CRUD import utils @@ -25,11 +25,7 @@ class User(Base): return utils.toDict(self) async def save(self) -> "User": - from db import CRUD - return await CRUD.create(self, refresh=True) async def edit(self, **kwargs) -> "User": - from db import CRUD - return await CRUD.update(User, self.id, **kwargs) diff --git a/main.py b/main.py index c771ae3..ea265b6 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,19 @@ -from utils import logger, setLogLevel +from utils import logger -def main(): - setLogLevel("WARNING") - logger.info("Приложение запущено") +async def main(): + from db import DATABASE_URL + from db.initialize import DatabaseInitializer - logger.info("Получение данных из базы...") - logger.warning({"query": "SELECT * FROM tools", "status": "slow"}) - setLogLevel("INFO") - logger.info("Пользователь открыл страницу настроек") - logger.debug(["Ошибка загрузки интерфейса", 502, "Bad Gateway"]) - setLogLevel("DEBUG") - test_data = {"a": 1, "b": 2, "c": [10, 20]} - logger.info(test_data) - - logger.debug("Приложение завершено") + try: + force = True + reNewDB = True + await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB) + except Exception as e: + logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True) if __name__ == "__main__": - main() \ No newline at end of file + import asyncio + + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 31181b4..e2bf2b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,10 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "argon2-cffi>=25.1.0", + "asyncpg>=0.31.0", "colorlog>=6.10.1", - "passlib>=1.7.4", + "greenlet>=3.2.4", "pillow>=12.0.0", "python-dotenv>=1.2.1", "sqlalchemy>=2.0.44", diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc index 0d862db..7bbd224 100644 Binary files a/utils/__pycache__/__init__.cpython-313.pyc and b/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/utils/__pycache__/for_DB.cpython-313.pyc b/utils/__pycache__/for_DB.cpython-313.pyc index df9ad02..e8ae215 100644 Binary files a/utils/__pycache__/for_DB.cpython-313.pyc and b/utils/__pycache__/for_DB.cpython-313.pyc differ diff --git a/utils/__pycache__/password.cpython-313.pyc b/utils/__pycache__/password.cpython-313.pyc index c4a49de..d1ebbe7 100644 Binary files a/utils/__pycache__/password.cpython-313.pyc and b/utils/__pycache__/password.cpython-313.pyc differ diff --git a/utils/__pycache__/safe_filename.cpython-313.pyc b/utils/__pycache__/safe_filename.cpython-313.pyc new file mode 100644 index 0000000..542d9ec Binary files /dev/null and b/utils/__pycache__/safe_filename.cpython-313.pyc differ diff --git a/utils/for_DB.py b/utils/for_DB.py index ba631e4..0bb10a4 100644 --- a/utils/for_DB.py +++ b/utils/for_DB.py @@ -1,4 +1,9 @@ def toDict(data) -> dict: + def dateToStr(date): + if date is None: + return None + return date.strftime("%Y-%m-%d %H:%M:%S") + return { c.name: ( ( @@ -7,7 +12,7 @@ def toDict(data) -> dict: else getattr(data, c.name).toDict() ) if not c.name.endswith("_at") - else getattr(data, c.name).strftime("%Y-%m-%d %H:%M:%S") + else dateToStr(getattr(data, c.name)) ) for c in data.__table__.columns } diff --git a/utils/password.py b/utils/password.py index b8327c9..2fe84da 100644 --- a/utils/password.py +++ b/utils/password.py @@ -1,18 +1,32 @@ -from passlib.context import CryptContext +from argon2 import PasswordHasher +from argon2.exceptions import ( + VerifyMismatchError, + VerificationError, + InvalidHash, +) + +from utils.loggers import logger -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +pwd_hasher = PasswordHasher( + time_cost=3, + memory_cost=65536, + parallelism=2, +) -def pwd_hash(pwd_plant: str) -> str: - return pwd_context.encrypt(pwd_plant) +def pwd_hash(pwd_plain: str) -> str: + pwd_plain = str(pwd_plain) + return pwd_hasher.hash(pwd_plain) -def pwd_verify(pwd_plant: str, pwd_hash: str) -> bool: - from utils.loggers import logger +def pwd_verify(pwd_plain: str, stored_hash: str) -> bool: + pwd_plain = str(pwd_plain) try: - return pwd_context.verify(pwd_plant, pwd_hash) - except Exception as e: - logger.error(f"Password verification error: {str(e)}") + valid = pwd_hasher.verify(stored_hash, pwd_plain) + except (VerifyMismatchError, VerificationError, InvalidHash) as e: + logger.warning(f"Password verification failed: {e.__class__.__name__}") return False + + return valid diff --git a/uv.lock b/uv.lock index 3266a11..e1ce415 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,130 @@ version = 1 revision = 3 requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] [[package]] name = "colorama" @@ -51,15 +175,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] -[[package]] -name = "passlib" -version = "1.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, -] - [[package]] name = "pillow" version = "12.0.0" @@ -118,6 +233,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -153,8 +277,10 @@ name = "tools-stock" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "argon2-cffi" }, + { name = "asyncpg" }, { name = "colorlog" }, - { name = "passlib" }, + { name = "greenlet" }, { name = "pillow" }, { name = "python-dotenv" }, { name = "sqlalchemy" }, @@ -162,8 +288,10 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "argon2-cffi", specifier = ">=25.1.0" }, + { name = "asyncpg", specifier = ">=0.31.0" }, { name = "colorlog", specifier = ">=6.10.1" }, - { name = "passlib", specifier = ">=1.7.4" }, + { name = "greenlet", specifier = ">=3.2.4" }, { name = "pillow", specifier = ">=12.0.0" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "sqlalchemy", specifier = ">=2.0.44" },