Создание и первичная инициализация базы даных успешно завершена. Наполнение демо-данными прошло без ошибок

This commit is contained in:
2025-12-06 12:58:42 +03:00
parent f378de38da
commit f07843de5a
49 changed files with 734 additions and 353 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
DB_HOST=10.0.13.3 DB_HOST=10.0.13.3
DB_PORT=5432 DB_PORT=5432
DB_NAME=tools_stock DB_NAME=toolbox
DB_USER=tools_stock DB_USER=toolbox
DB_PASS=z7kWLkSKa6 DB_PASS=z7kWLkSKa6
Binary file not shown.
+1 -1
View File
@@ -108,7 +108,7 @@ class CRUD:
item = await db.execute(query) item = await db.execute(query)
await db.commit() await db.commit()
logger.info("Запись обновлена") logger.info("Запись обновлена")
return item return await db.get(db_data, id)
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
logger.error(f"Ошибка обновления: {str(e)}", exc_info=True) logger.error(f"Ошибка обновления: {str(e)}", exc_info=True)
Binary file not shown.
Binary file not shown.
-45
View File
@@ -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",
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8 -8
View File
@@ -1,13 +1,13 @@
from sqlalchemy import select from sqlalchemy import select
from utils import logger from utils import logger
from db import CRUD from db import CRUD
from db.schemas import AccessLevel from db.schemas.access import AccessLevel
from db.handlers import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
class AccessLevelHandler: class AccessLevelHandler:
async def add(**kwargs): async def add(newData):
title = kwargs.get("title", None) title = newData.get("title", None)
if not title: if not title:
logger.error("Не указано название уровня доступа") logger.error("Не указано название уровня доступа")
return {} return {}
@@ -17,8 +17,8 @@ class AccessLevelHandler:
return {} return {}
try: try:
logger.info(f"Создание уровня доступа {title}") logger.info(f"Создание уровня доступа {title}")
user_id = kwargs.pop("user_id", None) user_id = newData.pop("user_id", None)
accessData = await AccessLevel(**kwargs).save() accessData = await AccessLevel(**newData).save()
await ServiceRecordsHandler.add( await ServiceRecordsHandler.add(
user_id, {"Добавлен уровень доступа": accessData.toDict()} user_id, {"Добавлен уровень доступа": accessData.toDict()}
) )
@@ -85,7 +85,7 @@ class AccessLevelHandler:
) )
return result return result
async def initialize(self): async def initialize():
baseAcessLevels = { baseAcessLevels = {
"admin": { "admin": {
"title": "Администратор", "title": "Администратор",
@@ -149,7 +149,7 @@ class AccessLevelHandler:
logger.info("Инициализация уровней доступа") logger.info("Инициализация уровней доступа")
for accessLevel in baseAcessLevels.values(): for accessLevel in baseAcessLevels.values():
await self.add(**accessLevel) await AccessLevelHandler.add(accessLevel)
logger.info("Уровни доступа успешно инициализированы") logger.info("Уровни доступа успешно инициализированы")
return return
+285 -38
View File
@@ -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 from utils import logger
@@ -32,7 +39,7 @@ class StocksActions:
recorded = await StocksRecordsHandler.add( recorded = await StocksRecordsHandler.add(
action="Оприходование", action="Оприходование",
source_stock_id=None, source_stock_id=None,
target_stock_id=newStocks.id, target_stock_id=newStocks.get("id"),
source_toolbox_id=None, source_toolbox_id=None,
target_toolbox_id=toolbox_id, target_toolbox_id=toolbox_id,
toolkit_id=toolkit_id, toolkit_id=toolkit_id,
@@ -46,7 +53,7 @@ class StocksActions:
f"Оприходование инструмента {toolkit_id} на складе {toolbox_id} прошло {'успешно' if recorded else 'не успешно'}" f"Оприходование инструмента {toolkit_id} на складе {toolbox_id} прошло {'успешно' if recorded else 'не успешно'}"
) )
if recorded: if recorded:
accepted = await StocksRecordsHandler.accept( accepted = await StocksRecordsHandler.decide(
recorded, user_id, None, quantity, price recorded, user_id, None, quantity, price
) )
if not accepted: if not accepted:
@@ -84,7 +91,7 @@ class StocksActions:
return False return False
totalTakeQuantity = 0 totalTakeQuantity = 0
writeDownList = [] movementsList = []
for stock in availability: for stock in availability:
if quantity == totalTakeQuantity: if quantity == totalTakeQuantity:
@@ -108,14 +115,15 @@ class StocksActions:
f"Списание инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} в количестве {takeQuantity} по цене {stock['price']} успешно завершена" f"Списание инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} в количестве {takeQuantity} по цене {stock['price']} успешно завершена"
) )
if not target_toolbox_id: movementsList.append(
writeDownList.append(
{ {
"id": sourceEdit["id"], "id": sourceEdit["id"],
"quantity": takeQuantity, "quantity": takeQuantity,
"price": stock["price"], "price": stock["price"],
} }
) )
if not target_toolbox_id:
continue continue
existing = await StockHandler.getByToolboxIdAndToolkitIdAndQPrice( existing = await StockHandler.getByToolboxIdAndToolkitIdAndQPrice(
@@ -168,7 +176,6 @@ class StocksActions:
reason=reason, reason=reason,
quantity=quantity, quantity=quantity,
price=stock["price"], price=stock["price"],
target_placement=target_placement,
) )
if not recorded: if not recorded:
logger.error( logger.error(
@@ -179,7 +186,7 @@ class StocksActions:
logger.info( logger.info(
f"{action} инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} прошло успешно" f"{action} инструмента {toolkit_id} со склада {source_toolbox_id} на склад {target_toolbox_id} прошло успешно"
) )
return True if target_toolbox_id else writeDownList return movementsList
async def movingRequest( async def movingRequest(
action: str, action: str,
@@ -213,29 +220,50 @@ class StocksActions:
) )
return recorded return recorded
async def movingAcceptance(self, record_id: int, user_id: int): async def movingDecision(record_id: int, user_id: int, accepted: bool = True):
logger.info(f"Принятие записи о движении инструмента {record_id} ...") logger.info(
writeDownARecord = await StocksRecordsHandler.getById(record_id, True) f"{'Принятие' if accepted else 'Отклонение'} записи о движении инструмента {record_id} ..."
if not writeDownARecord: )
movingRecord = await StocksRecordsHandler.getById(record_id, True)
if not movingRecord:
logger.error(f"Запись {record_id} не найдена") logger.error(f"Запись {record_id} не найдена")
return False return False
if writeDownARecord.accepted_at is not None: if movingRecord.accepted is not None:
logger.error(f"Запись {record_id} уже была принята") logger.error(
f"Запись {record_id} уже была {'принята' if movingRecord.accepted else 'отклонена'}"
)
return False return False
stocksMovements = await self.moving( if not accepted:
action=writeDownARecord.action, return await StocksRecordsHandler.decide(
source_toolbox_id=writeDownARecord.source_toolbox_id, record_id,
target_toolbox_id=writeDownARecord.target_toolbox_id, user_id,
toolkit_id=writeDownARecord.toolkit_id, movingRecord.source_stock_id,
quantity=writeDownARecord.quantity, 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, user_id=user_id,
reason=writeDownARecord.reason, reason=movingRecord.reason,
) )
if not stocksMovements: if not stocksMovements:
logger.error(f"Ошибка при {writeDownARecord.action} инструмента") logger.error(f"Ошибка при {movingRecord.action} инструмента")
return False return False
accept = await StocksRecordsHandler.accept( accept = await StocksRecordsHandler.decide(
record_id, record_id,
user_id, user_id,
stocksMovements[0].get("id"), stocksMovements[0].get("id"),
@@ -250,23 +278,23 @@ class StocksActions:
if len(stocksMovements) > 1: if len(stocksMovements) > 1:
for stock in stocksMovements[1:]: for stock in stocksMovements[1:]:
recorded = await StocksRecordsHandler.add( recorded = await StocksRecordsHandler.add(
action=writeDownARecord.action, action=movingRecord.action,
source_stock_id=stock.get("id"), source_stock_id=stock.get("id"),
target_stock_id=None, target_stock_id=None,
source_toolbox_id=writeDownARecord.source_toolbox_id, source_toolbox_id=movingRecord.source_toolbox_id,
target_toolbox_id=writeDownARecord.target_toolbox_id, target_toolbox_id=target_toolbox_id,
toolkit_id=writeDownARecord.toolkit_id, toolkit_id=movingRecord.toolkit_id,
init_user_id=writeDownARecord.init_user_id, init_user_id=movingRecord.init_user_id,
reason=writeDownARecord.reason, reason=movingRecord.reason,
quantity=stock.get("quantity"), quantity=stock.get("quantity"),
price=stock.get("price"), price=stock.get("price"),
return_record_id=True, return_record_id=True,
) )
if not recorded: if not recorded:
return False return False
accept = await StocksRecordsHandler.accept( accept = await StocksRecordsHandler.decide(
record_id=recorded, record_id=recorded,
accept_user_id=user_id, decision_user_id=user_id,
source_stock_id=stock.get("id"), source_stock_id=stock.get("id"),
quantity=stock.get("quantity"), quantity=stock.get("quantity"),
price=stock.get("price"), price=stock.get("price"),
@@ -276,12 +304,11 @@ class StocksActions:
totalRecordsIds.append(recorded) totalRecordsIds.append(recorded)
logger.info( logger.info(
f"Записи {', '.join(map(str, totalRecordsIds))} о {writeDownARecord.action} инструмента успешно приняты {user_id}" f"Записи {', '.join(map(str, totalRecordsIds))} о {movingRecord.action} инструмента успешно приняты {user_id}"
) )
return True return True
async def takeToolkit( async def takeToolkit(
self,
source_toolbox_id: int, source_toolbox_id: int,
target_toolbox_id: int, target_toolbox_id: int,
toolkit_id: int, toolkit_id: int,
@@ -293,7 +320,7 @@ class StocksActions:
logger.info( logger.info(
f"Формирование запроса на получение инструмента {toolkit_id} на склад {target_toolbox_id} со склада {source_toolbox_id} в количестве {quantity} ..." f"Формирование запроса на получение инструмента {toolkit_id} на склад {target_toolbox_id} со склада {source_toolbox_id} в количестве {quantity} ..."
) )
takeRequest = await self.movingRequest( takeRequest = await StocksActions.movingRequest(
action="Получение", action="Получение",
source_toolbox_id=source_toolbox_id, source_toolbox_id=source_toolbox_id,
target_toolbox_id=target_toolbox_id, target_toolbox_id=target_toolbox_id,
@@ -315,7 +342,7 @@ class StocksActions:
logger.info( logger.info(
f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} ..." f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} ..."
) )
accepted = await self.movingAcceptance(takeRequest, user_id) accepted = await StocksActions.movingDecision(takeRequest, user_id)
if not accepted: if not accepted:
logger.error( logger.error(
f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} не удалось" f"Принятие запроса {takeRequest} на получение инструмента {toolkit_id} не удалось"
@@ -327,6 +354,226 @@ class StocksActions:
return True return True
async def initialize(): async def initialize():
# TODO прописать наполнение общих складов, получение на личные, возвраты и списания.
# Не все запросы на возвраты и списания нужно принять автоматически, нужно оставить несколько для демонстрации toolboxes = await ToolboxHandler.getAll()
pass 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
+2 -2
View File
@@ -1,8 +1,8 @@
from sqlalchemy import select from sqlalchemy import select
from utils import logger from utils import logger
from db import CRUD from db import CRUD
from db.schemas import Category from db.schemas.categories import Category
from db.handlers import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
class CategoryHandler: class CategoryHandler:
+19 -11
View File
@@ -2,7 +2,8 @@ from datetime import datetime, timedelta
from sqlalchemy import select 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 from utils import logger
@@ -44,28 +45,37 @@ class StocksRecordsHandler:
logger.error(f"Ошибка создания записи: {str(e)}") logger.error(f"Ошибка создания записи: {str(e)}")
return False return False
async def accept( async def decide(
record_id: int, record_id: int,
accept_user_id: int, decision_user_id: int,
source_stock_id: int, source_stock_id: int,
quantity: int, quantity: int,
price: float, price: float,
accept: bool = True,
): ):
try: try:
logger.info(f"Принятие записи {record_id} от {accept_user_id}") logger.info(
record = await StocksRecords.get(id=record_id) f"{'Принятие' if accept else 'Отклонение'} записи {record_id} от {decision_user_id}"
record.accept_user_id = accept_user_id )
record.accepted_at = datetime.now() 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.source_stock_id = source_stock_id
record.quantity = quantity record.quantity = quantity
record.price = price record.price = price
record.accepted = accept
await record.save() await record.save()
logger.info( 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 return True
except Exception as e: except Exception as e:
logger.error(f"Ошибка принятия записи: {str(e)}") logger.error(
f"Ошибка {'принятия' if accept else 'отклонения'} записи: {str(e)}"
)
return False return False
async def edit(record_id: int, edit_user_id: int, **kwargs): async def edit(record_id: int, edit_user_id: int, **kwargs):
@@ -77,8 +87,6 @@ class StocksRecordsHandler:
return recordDB return recordDB
try: try:
from db.handlers.stock import StockHandler
logger.info(f"Обновление записи {record_id} от {edit_user_id}") logger.info(f"Обновление записи {record_id} от {edit_user_id}")
record = await StocksRecords.get(id=record_id) record = await StocksRecords.get(id=record_id)
+2 -2
View File
@@ -1,5 +1,5 @@
from sqlalchemy import select from sqlalchemy import select
from db.schemas import Stock from db.schemas.stock import Stock
from utils import logger from utils import logger
@@ -45,7 +45,7 @@ class StockHandler:
stock = await CRUD.read(select(Stock).where(Stock.id == stockId)) stock = await CRUD.read(select(Stock).where(Stock.id == stockId))
if not stock: if not stock:
logger.error("Запись об остатках не найдена") logger.error(f"Запись {stockId} об остатках не найдена")
return {} return {}
return filterQuantity(stock, filtered) if not record else stock return filterQuantity(stock, filtered) if not record else stock
+2 -2
View File
@@ -1,8 +1,8 @@
from utils import logger from utils import logger
from db import CRUD from db import CRUD
from db.schemas import Toolbox from db.schemas.toolbox import Toolbox
from sqlalchemy import or_, select from sqlalchemy import or_, select
from db.handlers import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
class ToolboxHandler: class ToolboxHandler:
+9 -11
View File
@@ -1,8 +1,8 @@
from utils import logger, saveImage, safeFilename, deleteImage from utils import logger, saveImage, safeFilename, deleteImage
from db import CRUD from db import CRUD
from db.schemas import Toolkit from db.schemas.toolkit import Toolkit
from sqlalchemy import select from sqlalchemy import select
from db.handlers import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
def handleToolkitImage(imageData, title: str): def handleToolkitImage(imageData, title: str):
@@ -51,10 +51,8 @@ class ToolkitHandler:
return {} return {}
logger.info(f"Инструмент {newToolkit.title} успешно создан") logger.info(f"Инструмент {newToolkit.title} успешно создан")
await ServiceRecordsHandler.add( await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData})
user_id, {"Добавлен инструмент": toolkitData.toDict()} return newToolkit
)
return newToolkit.toDict()
async def edit(toolkitId: int, **kwargs): async def edit(toolkitId: int, **kwargs):
query = select(Toolkit).where(Toolkit.id == toolkitId) query = select(Toolkit).where(Toolkit.id == toolkitId)
@@ -155,7 +153,7 @@ class ToolkitHandler:
) )
return result return result
async def initialize(self): async def initialize():
from .categories import CategoryHandler from .categories import CategoryHandler
logger.info("Инициализация инструментов") logger.info("Инициализация инструментов")
@@ -203,7 +201,7 @@ class ToolkitHandler:
"external_link": "https://nazv.ru", "external_link": "https://nazv.ru",
}, },
{ {
"title": "Псластина №1", "title": "Пластина №1",
"description": "Пластина такая сякая этакая #1", "description": "Пластина такая сякая этакая #1",
"specifications": { "specifications": {
"Размер": "10", "Размер": "10",
@@ -216,7 +214,7 @@ class ToolkitHandler:
"external_link": "https://nazv.ru", "external_link": "https://nazv.ru",
}, },
{ {
"title": "Псластина №2", "title": "Пластина №2",
"description": "Пластина такая сякая этакая #2", "description": "Пластина такая сякая этакая #2",
"specifications": { "specifications": {
"Размер": "10", "Размер": "10",
@@ -229,7 +227,7 @@ class ToolkitHandler:
"external_link": "https://nazv.ru", "external_link": "https://nazv.ru",
}, },
{ {
"title": "Псластина №3", "title": "Пластина №3",
"description": "Пластина такая сякая этакая #3", "description": "Пластина такая сякая этакая #3",
"specifications": { "specifications": {
"Размер": "10", "Размер": "10",
@@ -283,7 +281,7 @@ class ToolkitHandler:
] ]
for toolkit in baseToolkits: for toolkit in baseToolkits:
await self.add(toolkit) await ToolkitHandler.add(toolkit)
logger.info("Базовые инструменты успешно созданы") logger.info("Базовые инструменты успешно созданы")
return return
+26 -17
View File
@@ -1,9 +1,10 @@
from sqlalchemy import or_, select from sqlalchemy import or_, select
from db import CRUD 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 utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify
from db.schemas import User from db.schemas.user import User
from db.handlers import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
def handleUserPhoto(imageData, login: str): def handleUserPhoto(imageData, login: str):
@@ -52,20 +53,26 @@ class UserHandler:
logger.info( logger.info(
f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}" 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 = { newToolboxData = {
"title": f"Тулбокс {newUser.username}", "title": f"Тулбокс {newUser.username}",
"description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность", "description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность",
"owner_id": newUser.id, "owner_id": newUser.id,
} }
newToolbox = await addNewToolbox(newToolboxData) newToolbox = await ToolboxHandler.add(newToolboxData)
logger.info( logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}" f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
) )
await ServiceRecordsHandler.add( await ServiceRecordsHandler.add(
user_id, {"Добавлен пользователь": newUser.toDict()} 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: async def edit(userData: dict, user_id: int = None) -> dict:
id = userData.get("id", None) id = userData.get("id", None)
@@ -107,7 +114,7 @@ class UserHandler:
"description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность", "description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность",
"owner_id": editedUser.id, "owner_id": editedUser.id,
} }
newToolbox = await addNewToolbox(newToolboxData) newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData)
logger.info( logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
) )
@@ -182,7 +189,7 @@ class UserHandler:
) )
return userData return userData
async def initialize(self): async def initialize():
from .access import AccessLevelHandler from .access import AccessLevelHandler
logger.info("Инициализация пользователей") logger.info("Инициализация пользователей")
@@ -190,34 +197,36 @@ class UserHandler:
acessLevels = { acessLevels = {
accessLevel["title"]: accessLevel["id"] for accessLevel in accessLevelsList accessLevel["title"]: accessLevel["id"] for accessLevel in accessLevelsList
} }
logger.info(acessLevels)
password = "Alex0172"
baseUsers = { baseUsers = {
"admin": { "admin": {
"login": "admin", "login": "admin",
"username": "Администратор", "username": "Администратор - Demo",
"password": "Alex0172", "password": password,
"access_level_id": acessLevels["Администратор"], "access_level_id": acessLevels["Администратор"],
}, },
"manager": { "manager": {
"login": "manager", "login": "manager",
"username": "Менеджер", "username": "Менеджер - Demo",
"password": "Alex0172", "password": password,
"access_level_id": acessLevels["Менеджер"], "access_level_id": acessLevels["Менеджер"],
}, },
"storekeeper": { "storekeeper": {
"login": "storekeeper", "login": "storekeeper",
"username": "Кладовщик", "username": "Кладовщик - Demo",
"password": "Alex0172", "password": password,
"access_level_id": acessLevels["Кладовщик"], "access_level_id": acessLevels["Кладовщик"],
}, },
"employee": { "employee": {
"login": "employee", "login": "employee",
"username": "Сотрудник", "username": "Сотрудник - Demo",
"password": "Alex0172", "password": password,
"access_level_id": acessLevels["Сотрудник"], "access_level_id": acessLevels["Сотрудник"],
}, },
} }
for user in baseUsers.values(): for user in baseUsers.values():
await self.add(user) await UserHandler.add(user)
logger.info("Инициализация пользователей завершена") logger.info("Инициализация пользователей завершена")
return return
+153
View File
@@ -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
-128
View File
@@ -5,134 +5,6 @@ from .categories import *
from .toolbox import * from .toolbox import *
from .stock 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__ = [ __all__ = [
"User", "User",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -5
View File
@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text
from db import Base from db import Base, CRUD
import utils import utils
@@ -44,11 +44,7 @@ class AccessLevel(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(self, **kwargs): async def edit(self, **kwargs):
from db import CRUD
return await CRUD.update(AccessLevel, self.id, **kwargs) return await CRUD.update(AccessLevel, self.id, **kwargs)
+1 -5
View File
@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String, Text from sqlalchemy import Column, DateTime, Integer, String, Text
from db import Base from db import Base, CRUD
import utils import utils
@@ -21,11 +21,7 @@ class Category(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(id: int, **kwargs): async def edit(id: int, **kwargs):
from db import CRUD
return await CRUD.update(Category, id, **kwargs) return await CRUD.update(Category, id, **kwargs)
+14 -10
View File
@@ -1,7 +1,16 @@
from datetime import datetime 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 sqlalchemy.dialects.postgresql import JSONB
from db import Base from db import Base, CRUD
import utils import utils
@@ -28,18 +37,19 @@ class StocksRecords(Base):
init_user_id = Column( init_user_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
) )
accept_user_id = Column( decision_user_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
) )
reason = Column(Text, nullable=False) reason = Column(Text, nullable=False)
quantity = Column(Integer, nullable=False) quantity = Column(Integer, nullable=False)
price = Column(Float, nullable=False) price = Column(Float, nullable=False)
accepted = Column(Boolean, default=None, nullable=True)
edit_user_id = Column( edit_user_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
) )
edited = Column(JSONB, nullable=True) edited = Column(JSONB, nullable=True)
created_at = Column(DateTime, default=datetime.now) 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) edited_at = Column(DateTime, nullable=True)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -51,13 +61,9 @@ class StocksRecords(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(self, **kwargs): async def edit(self, **kwargs):
from db import CRUD
return await CRUD.update(StocksRecords, self.id, **kwargs) return await CRUD.update(StocksRecords, self.id, **kwargs)
@@ -77,6 +83,4 @@ class ServicesRecords(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
+11 -7
View File
@@ -2,7 +2,7 @@ from datetime import datetime
from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base from db import Base, CRUD
import utils import utils
@@ -14,13 +14,21 @@ class Stock(Base):
Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False
) )
toolkit_data = relationship( 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( toolbox_id = Column(
Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False
) )
toolbox_data = relationship( 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) quantity = Column(Integer, nullable=False)
price = Column(Float, nullable=False) price = Column(Float, nullable=False)
@@ -36,11 +44,7 @@ class Stock(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(self, **kwargs): async def edit(self, **kwargs):
from db import CRUD
return await CRUD.update(Stock, self.id, **kwargs) return await CRUD.update(Stock, self.id, **kwargs)
+1 -5
View File
@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from db import Base from db import Base, CRUD
import utils import utils
@@ -25,11 +25,7 @@ class Toolbox(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(id: int, **kwargs): async def edit(id: int, **kwargs):
from db import CRUD
return await CRUD.update(Toolbox, id, **kwargs) return await CRUD.update(Toolbox, id, **kwargs)
+6 -6
View File
@@ -2,7 +2,7 @@ from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base from db import Base, CRUD
import utils import utils
@@ -15,7 +15,11 @@ class Toolkit(Base):
specifications = Column(JSONB, default={}) specifications = Column(JSONB, default={})
category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE")) category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE"))
category_data = relationship( 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) image = Column(JSONB)
quantity_min = Column(Integer, nullable=True) quantity_min = Column(Integer, nullable=True)
@@ -34,11 +38,7 @@ class Toolkit(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self): async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(id: int, **kwargs): async def edit(id: int, **kwargs):
from db import CRUD
return await CRUD.update(Toolkit, id, **kwargs) return await CRUD.update(Toolkit, id, **kwargs)
+1 -5
View File
@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from db import Base from db import Base, CRUD
import utils import utils
@@ -25,11 +25,7 @@ class User(Base):
return utils.toDict(self) return utils.toDict(self)
async def save(self) -> "User": async def save(self) -> "User":
from db import CRUD
return await CRUD.create(self, refresh=True) return await CRUD.create(self, refresh=True)
async def edit(self, **kwargs) -> "User": async def edit(self, **kwargs) -> "User":
from db import CRUD
return await CRUD.update(User, self.id, **kwargs) return await CRUD.update(User, self.id, **kwargs)
+13 -15
View File
@@ -1,21 +1,19 @@
from utils import logger, setLogLevel from utils import logger
def main(): async def main():
setLogLevel("WARNING") from db import DATABASE_URL
logger.info("Приложение запущено") from db.initialize import DatabaseInitializer
logger.info("Получение данных из базы...") try:
logger.warning({"query": "SELECT * FROM tools", "status": "slow"}) force = True
setLogLevel("INFO") reNewDB = True
logger.info("Пользователь открыл страницу настроек") await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
logger.debug(["Ошибка загрузки интерфейса", 502, "Bad Gateway"]) except Exception as e:
setLogLevel("DEBUG") logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
test_data = {"a": 1, "b": 2, "c": [10, 20]}
logger.info(test_data)
logger.debug("Приложение завершено")
if __name__ == "__main__": if __name__ == "__main__":
main() import asyncio
asyncio.run(main())
+3 -1
View File
@@ -5,8 +5,10 @@ description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"argon2-cffi>=25.1.0",
"asyncpg>=0.31.0",
"colorlog>=6.10.1", "colorlog>=6.10.1",
"passlib>=1.7.4", "greenlet>=3.2.4",
"pillow>=12.0.0", "pillow>=12.0.0",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"sqlalchemy>=2.0.44", "sqlalchemy>=2.0.44",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -1
View File
@@ -1,4 +1,9 @@
def toDict(data) -> dict: def toDict(data) -> dict:
def dateToStr(date):
if date is None:
return None
return date.strftime("%Y-%m-%d %H:%M:%S")
return { return {
c.name: ( c.name: (
( (
@@ -7,7 +12,7 @@ def toDict(data) -> dict:
else getattr(data, c.name).toDict() else getattr(data, c.name).toDict()
) )
if not c.name.endswith("_at") 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 for c in data.__table__.columns
} }
+23 -9
View File
@@ -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: def pwd_hash(pwd_plain: str) -> str:
return pwd_context.encrypt(pwd_plant) pwd_plain = str(pwd_plain)
return pwd_hasher.hash(pwd_plain)
def pwd_verify(pwd_plant: str, pwd_hash: str) -> bool: def pwd_verify(pwd_plain: str, stored_hash: str) -> bool:
from utils.loggers import logger pwd_plain = str(pwd_plain)
try: try:
return pwd_context.verify(pwd_plant, pwd_hash) valid = pwd_hasher.verify(stored_hash, pwd_plain)
except Exception as e: except (VerifyMismatchError, VerificationError, InvalidHash) as e:
logger.error(f"Password verification error: {str(e)}") logger.warning(f"Password verification failed: {e.__class__.__name__}")
return False return False
return valid
Generated
+139 -11
View File
@@ -1,6 +1,130 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.13" 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]] [[package]]
name = "colorama" 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" }, { 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]] [[package]]
name = "pillow" name = "pillow"
version = "12.0.0" 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" }, { 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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.1" version = "1.2.1"
@@ -153,8 +277,10 @@ name = "tools-stock"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "argon2-cffi" },
{ name = "asyncpg" },
{ name = "colorlog" }, { name = "colorlog" },
{ name = "passlib" }, { name = "greenlet" },
{ name = "pillow" }, { name = "pillow" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
@@ -162,8 +288,10 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "argon2-cffi", specifier = ">=25.1.0" },
{ name = "asyncpg", specifier = ">=0.31.0" },
{ name = "colorlog", specifier = ">=6.10.1" }, { 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 = "pillow", specifier = ">=12.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "sqlalchemy", specifier = ">=2.0.44" }, { name = "sqlalchemy", specifier = ">=2.0.44" },