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

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_PORT=5432
DB_NAME=tools_stock
DB_USER=tools_stock
DB_NAME=toolbox
DB_USER=toolbox
DB_PASS=z7kWLkSKa6
Binary file not shown.
+1 -1
View File
@@ -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)
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 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
+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
@@ -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']} успешно завершена"
)
if not target_toolbox_id:
writeDownList.append(
movementsList.append(
{
"id": sourceEdit["id"],
"quantity": takeQuantity,
"price": stock["price"],
}
)
if not target_toolbox_id:
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
+2 -2
View File
@@ -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:
+19 -11
View File
@@ -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)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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:
+9 -11
View File
@@ -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
+26 -17
View File
@@ -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
+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 .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",
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 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)
+1 -5
View File
@@ -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)
+14 -10
View File
@@ -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)
+11 -7
View File
@@ -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)
+1 -5
View File
@@ -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)
+6 -6
View File
@@ -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)
+1 -5
View File
@@ -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)
+13 -15
View File
@@ -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()
import asyncio
asyncio.run(main())
+3 -1
View File
@@ -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",
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 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
}
+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:
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
Generated
+139 -11
View File
@@ -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" },