создана часть бекенда

This commit is contained in:
2025-11-30 19:31:06 +03:00
parent 4ae09c36b3
commit dc85e7c0c9
35 changed files with 1573 additions and 149 deletions
+21 -3
View File
@@ -1,20 +1,38 @@
[loggers] [loggers]
keys=root keys=root, tools_app, tools_db, tools_ui
[handlers] [handlers]
keys=logconsole keys=logconsole
[formatters] [formatters]
keys=formatter keys=formatter
encoding=utf-8
[logger_root] [logger_root]
level=INFO level=INFO
handlers=logconsole handlers=logconsole
[logger_tools_app]
level=INFO
handlers=logconsole
qualname=tools.app
propagate=0
[logger_tools_db]
level=INFO
handlers=logconsole
qualname=tools.db
propagate=0
[logger_tools_ui]
level=INFO
handlers=logconsole
qualname=tools.ui
propagate=0
[formatter_formatter] [formatter_formatter]
class=colorlog.ColoredFormatter class=colorlog.ColoredFormatter
format=%(log_color)s%(asctime)s: [%(levelname)s] %(message)s [%(module)s.%(funcName)s():%(lineno)d] format=%(log_color)s%(asctime)s: [%(levelname)s] %(message)s [%(filename)s:%(lineno)d '%(funcName)s']
datefmt=%Y-%m-%d %H:%M:%S datefmt=%Y-%m-%d %H:%M:%S
[handler_logconsole] [handler_logconsole]
+16 -16
View File
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.exc import InvalidRequestError from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
import config import config
from utils import loggerDB from utils import logger
DATABASE_URL = f"postgresql+asyncpg://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}" DATABASE_URL = f"postgresql+asyncpg://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}"
@@ -22,43 +22,43 @@ class CRUD:
is_lst = isinstance(db_data, list) is_lst = isinstance(db_data, list)
async with SessionLocal() as db: async with SessionLocal() as db:
if is_lst: if is_lst:
loggerDB.info(f"Создаю {len(db_data)} записей") logger.info(f"Создаю {len(db_data)} записей")
try: try:
db.add_all(db_data) db.add_all(db_data)
except InvalidRequestError: except InvalidRequestError:
for data in db_data: for data in db_data:
await db.merge(data) await db.merge(data)
else: else:
loggerDB.info("Создаю запись") logger.info("Создаю запись")
db.add(db_data) db.add(db_data)
await db.commit() await db.commit()
if refresh: if refresh:
if is_lst: if is_lst:
loggerDB.info(f"Обновляю {len(db_data)} записей") logger.info(f"Обновляю {len(db_data)} записей")
for data in db_data: for data in db_data:
await db.refresh(data) await db.refresh(data)
else: else:
loggerDB.info("Обновляю запись") logger.info("Обновляю запись")
await db.refresh(db_data) await db.refresh(db_data)
loggerDB.info("Запись создана") logger.info("Запись создана")
return db_data if refresh else None return db_data if refresh else None
except Exception as e: except Exception as e:
loggerDB.error(f"Ошибка создания: {str(e)}", exc_info=True) logger.error(f"Ошибка создания: {str(e)}", exc_info=True)
return None return None
async def read(query, all: bool = False): async def read(query, all: bool = False):
try: try:
async with SessionLocal() as db: async with SessionLocal() as db:
loggerDB.info(f"Чтение записей. Все: {all}") logger.info(f"Чтение записей. Все: {all}")
results = await db.execute(query) results = await db.execute(query)
loggerDB.info(f"Чтение завершено") logger.info(f"Чтение завершено")
return ( return (
results.unique().scalars().all() results.unique().scalars().all()
if all if all
else results.unique().scalars().first() else results.unique().scalars().first()
) )
except Exception as e: except Exception as e:
loggerDB.error(f"Ошибка чтения: {str(e)}", exc_info=True) logger.error(f"Ошибка чтения: {str(e)}", exc_info=True)
return None return None
async def delete(db_data) -> bool: async def delete(db_data) -> bool:
@@ -87,18 +87,18 @@ class CRUD:
async with SessionLocal() as db: async with SessionLocal() as db:
try: try:
if isinstance(db_data, list): if isinstance(db_data, list):
loggerDB.info(f"Удаляю записей: {len(db_data)}") logger.info(f"Удаляю записей: {len(db_data)}")
for data in db_data: for data in db_data:
await deleteFromDB(data, db) await deleteFromDB(data, db)
else: else:
loggerDB.info("Удаляю запись") logger.info("Удаляю запись")
await deleteFromDB(db_data, db) await deleteFromDB(db_data, db)
await db.commit() await db.commit()
loggerDB.info("Запись удалена") logger.info("Запись удалена")
return True return True
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
loggerDB.error(f"Ошибка удаления: {str(e)}", exc_info=True) logger.error(f"Ошибка удаления: {str(e)}", exc_info=True)
return False return False
async def update(db_data, id, **kwargs): async def update(db_data, id, **kwargs):
@@ -107,9 +107,9 @@ class CRUD:
query = update(db_data).where(db_data.id == id).values(**kwargs) query = update(db_data).where(db_data.id == id).values(**kwargs)
item = await db.execute(query) item = await db.execute(query)
await db.commit() await db.commit()
loggerDB.info("Запись обновлена") logger.info("Запись обновлена")
return item return item
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
loggerDB.error(f"Ошибка обновления: {str(e)}", exc_info=True) logger.error(f"Ошибка обновления: {str(e)}", exc_info=True)
return None return None
+39
View File
@@ -0,0 +1,39 @@
from .user import *
from .access import *
from .toolbox import *
from .categories import *
from .stock import *
from .toolkit import *
from .records 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()
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.stockHandler.initialize()
__all__ = [
"UserHandler",
"AccessLevelHandler",
"ToolboxHandler",
"CategoryHandler",
"StockHandler",
"ToolkitHandler",
"StocksRecords",
"ServicesRecords",
]
+131 -3
View File
@@ -2,9 +2,33 @@ 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 import AccessLevel
from db.handlers import ServiceRecordsHandler
async def getAccessdata(accessId: int) -> dict: class AccessLevelHandler:
async def add(**kwargs):
title = kwargs.get("title", None)
if not title:
logger.error("Не указано название уровня доступа")
return {}
exists = await CRUD.read(select(AccessLevel).where(AccessLevel.title == title))
if exists:
logger.error("Уровень доступа с таким названием уже существует")
return {}
try:
logger.info(f"Создание уровня доступа {title}")
user_id = kwargs.pop("user_id", None)
accessData = await AccessLevel(**kwargs).save()
await ServiceRecordsHandler.add(
user_id, {"Добавлен уровень доступа": accessData.toDict()}
)
except Exception as e:
logger.error(f"Ошибка создания уровня доступа: {str(e)}")
return {}
logger.info(f"Уровень доступа {accessData.title} успешно создан")
return accessData.toDict()
async def get(accessId: int) -> dict:
query = select(AccessLevel).where(AccessLevel.id == accessId) query = select(AccessLevel).where(AccessLevel.id == accessId)
accessData = await CRUD.read(query) accessData = await CRUD.read(query)
if not accessData: if not accessData:
@@ -12,15 +36,119 @@ async def getAccessdata(accessId: int) -> dict:
return {} return {}
return accessData.toDict() return accessData.toDict()
async def edit(accessId: int, **kwargs):
async def editAccessData(accessId: int, **kwargs):
query = select(AccessLevel).where(AccessLevel.id == accessId) query = select(AccessLevel).where(AccessLevel.id == accessId)
accessData = await CRUD.read(query) accessData = await CRUD.read(query)
if not accessData: if not accessData:
logger.error("Уровень доступа не найден") logger.error("Уровень доступа не найден")
return {} return {}
try:
user_id = kwargs.pop("user_id", None)
editedAccessData = await accessData.edit(**kwargs) editedAccessData = await accessData.edit(**kwargs)
await ServiceRecordsHandler.add(
user_id, {"Обновлен уровень доступа": editedAccessData.toDict()}
)
except Exception as e:
logger.error(f"Ошибка обновления уровня доступа: {str(e)}")
return {}
logger.info( logger.info(
f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}" f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}"
) )
return editedAccessData.toDict() return editedAccessData.toDict()
async def getAll() -> list:
query = select(AccessLevel)
accessLevels = await CRUD.read(query, True)
return (
[accessLevel.toDict() for accessLevel in accessLevels]
if accessLevels
else []
)
async def delete(accessId: int, user_id: int = None):
query = select(AccessLevel).where(AccessLevel.id == accessId)
accessData = await CRUD.read(query)
if not accessData:
logger.error("Уровень доступа не найден")
return False
try:
title = accessData.title
result = await CRUD.delete(accessData)
await ServiceRecordsHandler.add(
user_id, {"Удален уровень доступа": f"Название: {title}"}
)
except Exception as e:
logger.error(f"Ошибка удаления уровня доступа: {str(e)}")
return False
logger.info(
f"Уровень доступа {accessData.title} {'успешно удален' if result else 'не удален'}"
)
return result
async def initialize(self):
baseAcessLevels = {
"admin": {
"title": "Администратор",
"description": "Администратор. Полный доступ",
"receiving_edit": True,
"refund_request_edit": True,
"refund_request_confirm": True,
"debit_request_edit": True,
"debit_request_confirm": True,
"tools_creation": True,
"tools_registration": True,
"tools_registration_edit": True,
"tools_edit": True,
"tools_delete": True,
"users_creation": True,
"users_edit": True,
"users_disabling": True,
"users_view": True,
"available_own_toolbox": False,
"view_all_toolboxes": True,
"view_requests": True,
"view_services": True,
"access_level_view": True,
"access_level_edit": True,
"manage_toolboxes": True,
},
"manager": {
"title": "Менеджер",
"description": "Менеджер. Доступ к просмотру и редактированию рабочей информации",
"refund_request_confirm": True,
"debit_request_confirm": True,
"tools_creation": True,
"tools_registration": True,
"tools_edit": True,
"users_disabling": True,
"users_view": True,
"view_all_toolboxes": True,
"view_requests": True,
"view_services": True,
"access_level_view": True,
"manage_toolboxes": True,
},
"storekeeper": {
"title": "Кладовщик",
"description": "Кладовщик. Доступ к управлению складом",
"refund_request_confirm": True,
"debit_request_confirm": True,
"tools_creation": True,
"tools_registration": True,
"tools_edit": True,
"users_view": True,
"view_requests": True,
"view_all_toolboxes": True,
},
"employee": {
"title": "Сотрудник",
"description": "Сотрудник. Управление собственной рабочей информацией",
"available_own_toolbox": True,
},
}
for accessLevel in baseAcessLevels.values():
await self.add(**accessLevel)
logger.info("Уровни доступа успешно инициализированы")
return
+92
View File
@@ -0,0 +1,92 @@
from sqlalchemy import select
from utils import logger
from db import CRUD
from db.schemas import Category
from db.handlers import ServiceRecordsHandler
class CategoryHandler:
async def add(newCategoryData: dict, user_id: int = None):
title = newCategoryData.get("title", None)
if not title:
logger.error("Не указано название категории")
return {}
query = select(Category).where(Category.title == title)
category = await CRUD.read(query)
if category:
logger.error("Категория с таким названием уже существует")
return {}
try:
newCategory = await Category(**newCategoryData).save()
except Exception as e:
logger.error(f"Ошибка сохранения категории: {str(e)}")
return {}
if not newCategory:
logger.error("Категория не сохранена")
return {}
await ServiceRecordsHandler.add(
user_id, {"Добавлена категория": newCategory.toDict()}
)
logger.info(
f"Категория {newCategory.title} успешно добавлена, id: {newCategory.id}"
)
return newCategory.toDict()
async def edit(categoryId: int, **kwargs):
query = select(Category).where(Category.id == categoryId)
category = await CRUD.read(query)
if not category:
logger.error("Категория не найдена")
return {}
try:
user_id = kwargs.get("user_id", None)
editedCategory = await category.edit(**kwargs)
except Exception as e:
logger.error(f"Ошибка обновления категории: {str(e)}")
return {}
if not editedCategory:
logger.error("Категория не обновлена")
return {}
await ServiceRecordsHandler.add(
user_id, {f"Обновлена категория {category.title}": editedCategory.toDict()}
)
logger.info(f"Категория {editedCategory.title} успешно обновлена")
return editedCategory.toDict()
async def getAll() -> list[dict]:
query = select(Category)
categories = await CRUD.read(query, True)
return [category.toDict() for category in categories] if categories else []
async def delete(categoryId: int, user_id: int = None):
query = select(Category).where(Category.id == categoryId)
category = await CRUD.read(query)
if not category:
logger.error("Категория не найдена")
return False
try:
categoryTitle = category.title
result = await CRUD.delete(category)
except Exception as e:
logger.error(f"Ошибка удаления категории: {str(e)}")
return False
await ServiceRecordsHandler.add(
user_id, {"Удалена категория": f"Название: {categoryTitle}"}
)
logger.info(
f"Категория {categoryTitle} {'успешно удалена' if result else 'не удалена'}"
)
return result
async def initialize():
baseCategories = [
{"title": "Фрезеровка", "description": "Инструмент для фрезерного цеха"},
{"title": "Токарка", "description": "Инструмент для токарного цеха"},
{"title": "Слесарка", "description": "Инструмент для слесарного цеха"},
]
for categoryData in baseCategories:
await CategoryHandler.add(categoryData)
logger.info("Категории успешно созданы")
return
+118
View File
@@ -0,0 +1,118 @@
from datetime import datetime, timedelta
from sqlalchemy import select
from db.schemas import StocksRecords, ServicesRecords
from utils import logger
class StocksRecordsHandler:
async def add(
action: str,
source_toolbox_id: int,
target_toolbox_id: int,
toolkit_id: int,
init_user_id: int,
reason: str,
quantity: int,
):
recordData = {
"action": action,
"source_toolbox_id": source_toolbox_id,
"toolkit_id": toolkit_id,
"init_user_id": init_user_id,
"reason": reason,
"quantity": quantity,
}
if target_toolbox_id:
recordData["target_toolbox_id"] = target_toolbox_id
try:
logger.info(f"Создание записи: {action} от {init_user_id}")
logger.debug(recordData)
record = StocksRecords(**recordData)
await record.save()
logger.info(f"Запись успешно создана, id: {record.id}")
return True
except Exception as e:
logger.error(f"Ошибка создания записи: {str(e)}")
return False
async def accept(record_id: int, accept_user_id: int):
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()
await record.save()
logger.info(
f"Запись {record_id} успешно принята {accept_user_id} в {record.accepted_at.strftime('%Y-%m-%d %H:%M:%S')}"
)
return True
except Exception as e:
logger.error(f"Ошибка принятия записи: {str(e)}")
return False
async def edit(record_id: int, edit_user_id: int, **kwargs):
try:
logger.info(f"Обновление записи {record_id} от {edit_user_id}")
record = await StocksRecords.get(id=record_id)
record.edit_user_id = edit_user_id
record.edited_at = datetime.now()
edited = {}
for key, value in kwargs.items():
originalValue = getattr(record, key)
setattr(record, key, value)
edited[key] = {"original": originalValue, "new": value}
record.edited = edited
await record.save()
logger.info(
f"Запись {record_id} успешно обновлена {edit_user_id} в {record.updated_at.strftime('%Y-%m-%d %H:%M:%S')}"
)
logger.debug(edited)
return True
except Exception as e:
logger.error(f"Ошибка обновления записи: {str(e)}")
return False
async def get(user_id: int = None, days: int = 30):
from db import CRUD
try:
if user_id:
userInfo = f"пользователя {user_id} "
query = select(StocksRecords).where(
StocksRecords.init_user_id == user_id,
StocksRecords.created_at > datetime.now() - timedelta(days=days),
)
else:
userInfo = "всех пользователей "
query = select(StocksRecords).where(
StocksRecords.created_at > datetime.now() - timedelta(days=days),
)
logger.info(f"Получение всех записей {userInfo}за последние {days} дн.")
records = await CRUD.read(query, True)
logger.info(
f"{len(records)} записей {userInfo}за последние {days} дн. успешно получены"
)
if len(records) == 0:
return []
records.sort(key=lambda x: x.created_at, reverse=True)
recordsData = [record.toDict() for record in records]
logger.debug(recordsData)
return recordsData
except Exception as e:
logger.error(f"Ошибка получения записей: {str(e)}")
return False
class ServiceRecordsHandler:
async def add(user_id: int, details: dict):
try:
logger.info(f"Создание записи: {user_id}")
logger.debug(details)
record = ServicesRecords(user_id=user_id, details=details)
await record.save()
logger.info(f"Запись успешно создана, id: {record.id}")
return True
except Exception as e:
logger.error(f"Ошибка создания записи: {str(e)}")
return False
+88
View File
@@ -0,0 +1,88 @@
from sqlalchemy import select
from db.schemas import Stock
from utils import logger
def filterQuantity(stocksData, filtered):
def filterStock(stock):
if stock.quantity > 0:
return stock
else:
return False
if isinstance(stocksData, list):
if len(stocksData) == 0:
return []
stocksData.sort(key=lambda stock: stock.created_at)
filteredStocks = (
list(filter(filterStock, stocksData)) if filtered else stocksData
)
return [stock.toDict() for stock in filteredStocks] if filteredStocks else []
else:
stock = filterStock(stocksData) if filtered else stocksData
if stock:
return stock.toDict()
else:
return {}
class StockHandler:
async def add(**kwargs):
newStock = await Stock(**kwargs).save()
logger.info(
f"Новая запись об инструменте {newStock.toolkit_data.title} на складе {newStock.toolbox_data.title} успешно создана"
)
return newStock.toDict()
async def getAll(filtered: bool = True):
from db import CRUD
stocks = await CRUD.read(select(Stock), all=True)
return filterQuantity(stocks, filtered)
async def get(stockId: int, filtered: bool = True):
from db import CRUD
stock = await CRUD.read(select(Stock).where(Stock.id == stockId))
if not stock:
logger.error("Запись об остатках не найдена")
return {}
return filterQuantity(stock, filtered)
async def getByToolboxId(toolboxId: int, filtered: bool = True):
from db import CRUD
query = select(Stock).where(Stock.toolbox_id == toolboxId)
stocks = await CRUD.read(query, True)
return filterQuantity(stocks, filtered)
async def getByToolkitId(toolkitId: int, filtered: bool = True):
from db import CRUD
query = select(Stock).where(Stock.toolkit_id == toolkitId)
stocks = await CRUD.read(query, True)
return filterQuantity(stocks, filtered)
async def edit(stockId: int, **kwargs):
from db import CRUD
stock = await CRUD.read(select(Stock).where(Stock.id == stockId))
if not stock:
logger.error("Запись об остатках не найдена")
return {}
try:
stockInfo = f"инструмента {stock.toolkit_data.title} на складе {stock.toolbox_data.title}"
editedStock = await stock.edit(**kwargs)
except Exception as e:
logger.error(f"Ошибка обновления записи об остатках: {str(e)}")
return {}
if not editedStock:
logger.error("Запись об остатках не обновлена")
return {}
logger.info(
f"Запись об остатках {stockInfo} успешно обновлена, изменены данные: {kwargs.keys()}"
)
return editedStock.toDict()
async def initialize():
pass
+95 -1
View File
@@ -2,9 +2,11 @@ from utils import logger
from db import CRUD from db import CRUD
from db.schemas import Toolbox from db.schemas import Toolbox
from sqlalchemy import select from sqlalchemy import select
from db.handlers import ServiceRecordsHandler
async def addNewToolbox(toolboxData: dict): class ToolboxHandler:
async def add(toolboxData: dict, user_id: int = None):
title = toolboxData.get("title", None) title = toolboxData.get("title", None)
if not title: if not title:
logger.error("Не указано Назавание тулбокса") logger.error("Не указано Назавание тулбокса")
@@ -14,6 +16,98 @@ async def addNewToolbox(toolboxData: dict):
if toolbox: if toolbox:
logger.error("Тулбокс с таким названием уже существует") logger.error("Тулбокс с таким названием уже существует")
return {} return {}
try:
logger.info(f"Создание тулбокса {title}")
newToolbox = await Toolbox(**toolboxData).save() newToolbox = await Toolbox(**toolboxData).save()
except Exception as e:
logger.error(f"Ошибка сохранения тулбокса: {str(e)}")
return {}
if not newToolbox:
logger.error("Тулбокс не сохранен")
return {}
await ServiceRecordsHandler.add(
user_id, {"Добавлен тулбокс": newToolbox.toDict()}
)
logger.info(f"Тулбокс {newToolbox.title} успешно создан") logger.info(f"Тулбокс {newToolbox.title} успешно создан")
return newToolbox.toDict() return newToolbox.toDict()
async def edit(toolboxId: int, **kwargs):
query = select(Toolbox).where(Toolbox.id == toolboxId)
toolbox = await CRUD.read(query)
if not toolbox:
logger.error("Тулбокс не найден")
return {}
try:
user_id = kwargs.pop("user_id", None)
editedToolbox = await toolbox.edit(**kwargs)
except Exception as e:
logger.error(f"Ошибка обновления тулбокса: {str(e)}")
return {}
if not editedToolbox:
logger.error("Тулбокс не обновлен")
return {}
logger.info(
f"Тулбокс {editedToolbox.title} успешно обновлен, изменены данные: {kwargs.keys()}"
)
await ServiceRecordsHandler.add(
user_id, {f"Обновлен тулбокс {toolbox.title}": editedToolbox.toDict()}
)
return editedToolbox.toDict()
async def getAll() -> list:
query = select(Toolbox)
toolboxes = await CRUD.read(query, True)
return [toolbox.toDict() for toolbox in toolboxes] if toolboxes else []
async def get(toolboxId: int) -> dict:
query = select(Toolbox).where(Toolbox.id == toolboxId)
toolbox = await CRUD.read(query)
if not toolbox:
logger.error("Тулбокс не найден")
return {}
return toolbox.toDict()
async def delete(toolboxId: int, user_id: int = None):
query = select(Toolbox).where(Toolbox.id == toolboxId)
toolbox = await CRUD.read(query)
if not toolbox:
logger.error("Тулбокс не найден")
return False
try:
toolboxTitle = toolbox.title
result = await CRUD.delete(toolbox)
except Exception as e:
logger.error(f"Ошибка удаления тулбокса: {str(e)}")
return False
logger.info(
f"Тулбокс {toolboxTitle} {'успешно удален' if result else 'не удален'}"
)
await ServiceRecordsHandler.add(
user_id, {"Удален тулбокс": f"Название: {toolboxTitle}"}
)
return result
async def initialize():
baseToolsboxes = [
{
"title": "Стеллаж",
"description": "Основной стеллаж с режущим инструментом",
"owner_id": None,
"monitoring": True,
},
{
"title": "Шкаф",
"description": "Шкаф для хранения инструментов",
"owner_id": None,
"monitoring": True,
},
]
for toolboxData in baseToolsboxes:
await ToolboxHandler.add(toolboxData)
logger.info("Тулбоксы успешно созданы")
return
+288
View File
@@ -0,0 +1,288 @@
from utils import logger, saveImage, safeFilename, deleteImage
from db import CRUD
from db.schemas import Toolkit
from sqlalchemy import select
from db.handlers import ServiceRecordsHandler
def handleToolkitImage(imageData, title: str):
title = safeFilename(title)
fileName = f"tools/{title}.png"
if not saveImage(imageData, fileName):
return None
return fileName
class ToolkitHandler:
async def add(toolkitData: dict, user_id: int = None):
title = toolkitData.get("title", None)
if not title:
logger.error("Не указано название инструмента")
return {}
query = select(Toolkit).where(Toolkit.title == title)
toolkit = await CRUD.read(query)
if toolkit:
logger.error("Инструмент с таким названием уже существует")
return {}
try:
imageDict = {"main": "images/tools/default.png", "additional": []}
if "image" in toolkitData:
imageData = toolkitData.pop("image")
mainImage = imageData.get("main")
imageFileName = handleToolkitImage(mainImage, title)
if imageFileName:
imageDict["main"] = imageFileName
additionalImages = imageData.get("additional", [])
if len(additionalImages) > 0:
for image in additionalImages:
imageFileName = handleToolkitImage(image, title)
if imageFileName:
imageDict["additional"].append(imageFileName)
toolkitData["image"] = imageDict
newToolkit = await Toolkit(**toolkitData).save()
except Exception as e:
logger.error(f"Ошибка сохранения инструмента: {str(e)}")
return {}
if not newToolkit:
logger.error("Инструмент не сохранен")
return {}
logger.info(f"Инструмент {newToolkit.title} успешно создан")
await ServiceRecordsHandler.add(
user_id, {"Добавлен инструмент": toolkitData.toDict()}
)
return newToolkit.toDict()
async def edit(toolkitId: int, **kwargs):
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
if not toolkit:
logger.error("Инструмент не найден")
return {}
try:
if "image" in kwargs:
title = kwargs.get("title", toolkit.title)
imageData = kwargs.pop("image")
imageDict = {"main": "", "additional": []}
existImagesList = [toolkit.image.get("main")]
existImagesList.extend(toolkit.image.get("additional"))
newImagesList = [imageData.get("main")]
newImagesList.extend(imageData.get("additional"))
for existImage in existImagesList:
if existImage not in newImagesList:
deleteImage(existImage)
if toolkit.image.get("main") != imageData.get("main"):
if imageData.get("main") in existImagesList:
imageDict["main"] = imageData.get("main")
else:
imageFileName = handleToolkitImage(imageData.get("main"), title)
if imageFileName:
imageDict["main"] = imageFileName
else:
imageDict["main"] = "images/tools/default.png"
imageDict["additional"].extend(imageData.get("additional"))
uploadList = imageData.get("upload", [])
if len(uploadList) > 0:
for image in uploadList:
imageFileName = handleToolkitImage(image, title)
if imageFileName:
imageDict["additional"].append(imageFileName)
kwargs["image"] = imageDict
user_id = kwargs.pop("user_id", None)
editedToolkit = await toolkit.edit(**kwargs)
except Exception as e:
logger.error(f"Ошибка обновления инструмента: {str(e)}")
return {}
if not editedToolkit:
logger.error("Инструмент не обновлен")
return {}
logger.info(
f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}"
)
await ServiceRecordsHandler.add(
user_id, {f"Обновлен инструмент {toolkit.title}": editedToolkit.toDict()}
)
return editedToolkit.toDict()
async def getAll():
query = select(Toolkit)
toolkits = await CRUD.read(query, True)
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
async def get(toolkitId: int):
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
if not toolkit:
logger.error("Инструмент не найден")
return {}
return toolkit.toDict()
async def getSeveral(toolkitIds: list[int]) -> list[dict]:
query = select(Toolkit).where(Toolkit.id.in_(toolkitIds))
toolkits = await CRUD.read(query, True)
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
async def delete(toolkitId: int, user_id: int = None):
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
if not toolkit:
logger.error("Инструмент не найден")
return False
try:
toolkitTitle = toolkit.title
result = await CRUD.delete(toolkit)
except Exception as e:
logger.error(f"Ошибка удаления инструмента: {str(e)}")
return False
logger.info(
f"Инструмент {toolkitTitle} {'успешно удален' if result else 'не удален'}"
)
await ServiceRecordsHandler.add(
user_id, {"Удален инструмент": f"Название: {toolkitTitle}"}
)
return result
async def initialize(self):
from .categories import CategoryHandler
categoriesList = await CategoryHandler.getAll()
categories = {category["title"]: category["id"] for category in categoriesList}
baseToolkits = [
{
"title": "Фреза №1",
"description": "Фреза такая сякая этакая #1",
"specifications": {
"Диаметр": "10",
"Длина": "20",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Фрезеровка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Фреза №2",
"description": "Фреза такая сякая этакая #2",
"specifications": {
"Диаметр": "10",
"Длина": "20",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Фрезеровка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Фреза №3",
"description": "Фреза такая сякая этакая #3",
"specifications": {
"Диаметр": "10",
"Длина": "20",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Фрезеровка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Псластина №1",
"description": "Пластина такая сякая этакая #1",
"specifications": {
"Размер": "10",
"Радиус": "0.4",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Токарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Псластина №2",
"description": "Пластина такая сякая этакая #2",
"specifications": {
"Размер": "10",
"Радиус": "0.4",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Токарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Псластина №3",
"description": "Пластина такая сякая этакая #3",
"specifications": {
"Размер": "10",
"Радиус": "0.4",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Токарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Сверло №1",
"description": "Сверло такое сякое этакое #1",
"specifications": {
"Длина": "30",
"Диаметр": "5",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Слесарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Сверло №2",
"description": "Сверло такое сякое этакое #2",
"specifications": {
"Длина": "30",
"Диаметр": "5",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Слесарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
{
"title": "Сверло №3",
"description": "Сверло такое сякое этакое #3",
"specifications": {
"Длина": "30",
"Диаметр": "5",
"Ещё что-то": "Ещё столько-то",
},
"category_id": categories["Слесарка"],
"quantity_min": 20,
"quantity_min_extra": 10,
"external_link": "https://nazv.ru",
},
]
for toolkit in baseToolkits:
await self.add(toolkit)
logger.info("Базовые инструменты успешно созданы")
return
+149 -8
View File
@@ -1,16 +1,26 @@
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.toolbox import addNewToolbox
from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify
from db.schemas import User from db.schemas import User
from utils import logger, pwd_hash from db.handlers import ServiceRecordsHandler
async def addNewUser(userData: dict) -> dict: def handleUserPhoto(imageData, login: str):
login = safeFilename(login)
fileName = f"users/{login}.png"
if not saveImage(imageData, fileName):
return None
return fileName
class UserHandler:
async def add(userData: dict, user_id: int = None) -> dict:
login = userData.get("login", None) login = userData.get("login", None)
if not login: if not login:
logger.error("Не указан логин") logger.error("Не указан логин")
return {} return {}
userName = userData.get("username") userName = userData.get("username", None)
if not userName: if not userName:
logger.error("Не указано имя пользователя") logger.error("Не указано имя пользователя")
return {} return {}
@@ -26,11 +36,22 @@ async def addNewUser(userData: dict) -> dict:
logger.error("Не указан пароль") logger.error("Не указан пароль")
return {} return {}
userData["hashed_password"] = pwd_hash(userData.pop("password")) userData["hashed_password"] = pwd_hash(userData.pop("password"))
if "photo" in userData:
imageData = userData.pop("photo")
photoFile = handleUserPhoto(imageData, login)
if photoFile:
userData["photo"] = photoFile
try:
newUser = await User(**userData).save() newUser = await User(**userData).save()
if not newUser: except Exception as e:
logger.error("Ошибка сохранения пользователя") logger.error(f"Ошибка сохранения пользователя: {str(e)}")
return {} return {}
logger.info(f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}") if not newUser:
logger.error("Пользователь не сохранен")
return {}
logger.info(
f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}"
)
if newUser.available_own_toolbox: if newUser.available_own_toolbox:
newToolboxData = { newToolboxData = {
"title": f"Тулбокс {newUser.username}", "title": f"Тулбокс {newUser.username}",
@@ -41,10 +62,12 @@ async def addNewUser(userData: dict) -> dict:
logger.info( logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}" f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
) )
await ServiceRecordsHandler.add(
user_id, {"Добавлен пользователь": newUser.toDict()}
)
return newUser.toDict() return newUser.toDict()
async def edit(userData: dict, user_id: int = None) -> dict:
async def editUser(userData: dict) -> dict:
id = userData.get("id", None) id = userData.get("id", None)
if not id: if not id:
logger.error("Не указан id пользователя") logger.error("Не указан id пользователя")
@@ -60,7 +83,17 @@ async def editUser(userData: dict) -> dict:
return {} return {}
if "password" in changedUserData: if "password" in changedUserData:
userData["hashed_password"] = pwd_hash(changedUserData.pop("password")) userData["hashed_password"] = pwd_hash(changedUserData.pop("password"))
if "photo" in changedUserData:
imageData = changedUserData.pop("photo")
photoFile = handleUserPhoto(imageData, user.login)
if photoFile:
changedUserData["photo"] = photoFile
deleteImage(user.photo)
try:
editedUser = await user.edit(**changedUserData) editedUser = await user.edit(**changedUserData)
except Exception as e:
logger.error(f"Ошибка обновления пользователя: {str(e)}")
return {}
if not editedUser: if not editedUser:
logger.error("Ошибка обновления пользователя") logger.error("Ошибка обновления пользователя")
return {} return {}
@@ -78,4 +111,112 @@ async def editUser(userData: dict) -> dict:
logger.info( logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
) )
await ServiceRecordsHandler.add(
user_id, {"Изменен пользователь": editedUser.toDict()}
)
return editedUser.toDict() return editedUser.toDict()
async def getAll() -> list[dict]:
query = select(User)
users = await CRUD.read(query, True)
return [user.toDict() for user in users]
async def get(id: int) -> dict:
query = select(User).where(User.id == id)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким id не найден")
return {}
return user.toDict()
async def delete(id: int, user_id: int = None) -> bool:
query = select(User).where(User.id == id)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким id не найден")
return False
try:
userName = user.username
result = await CRUD.delete(user)
except Exception as e:
logger.error(f"Ошибка удаления пользователя: {str(e)}")
return False
logger.info(
f"Пользователь {userName} {'успешно удален' if result else 'не удален'}"
)
await ServiceRecordsHandler.add(user_id, {"Удален пользователь": userName})
return result
async def deletePhoto(id: int, user_id: int = None) -> bool:
query = select(User).where(User.id == id)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким id не найден")
return False
try:
deleteImage(user.photo)
user.photo = "images/users/default.png"
await user.save()
except Exception as e:
logger.error(f"Ошибка удаления фото пользователя: {str(e)}")
return False
logger.info(f"Фото пользователя {user.username} успешно удалено")
await ServiceRecordsHandler.add(
user_id, {"Удалено фото пользователя": user.username}
)
return True
async def auth(login: str, password: str) -> dict:
query = select(User).where(User.login == login)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким логином не найден")
return {}
if not pwd_verify(password, user.hashed_password):
logger.error("Неверный пароль")
return {}
userData = user.toDict()
userData.pop("hashed_password")
await ServiceRecordsHandler.add(
user.id, {"Авторизован пользователь": user.username}
)
return userData
async def initialize(self):
from .access import AccessLevelHandler
accessLevelsList = await AccessLevelHandler.getAll()
acessLevels = {
accessLevel["title"]: accessLevel["id"] for accessLevel in accessLevelsList
}
baseUsers = {
"admin": {
"login": "admin",
"username": "Администратор",
"password": "Alex0172",
"access_level_id": acessLevels["Администратор"],
},
"manager": {
"login": "manager",
"username": "Менеджер",
"password": "Alex0172",
"access_level_id": acessLevels["Менеджер"],
},
"storekeeper": {
"login": "storekeeper",
"username": "Кладовщик",
"password": "Alex0172",
"access_level_id": acessLevels["Кладовщик"],
},
"employee": {
"login": "employee",
"username": "Сотрудник",
"password": "Alex0172",
"access_level_id": acessLevels["Сотрудник"],
},
}
for user in baseUsers.values():
await self.add(user)
logger.info("Инициализация модуля пользователей завершена")
return
+144 -6
View File
@@ -1,6 +1,144 @@
from user import * from .user import *
from access import * from .access import *
from toolkit import * from .toolkit import *
from categories import * from .categories import *
from db.schemas.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__ = [
"User",
"Access",
"Toolbox",
"Category",
"Stock",
"Toolkit",
]
+2 -1
View File
@@ -23,7 +23,7 @@ class AccessLevel(Base):
tools_registration = Column(Boolean, default=False) tools_registration = Column(Boolean, default=False)
tools_registration_edit = Column(Boolean, default=False) tools_registration_edit = Column(Boolean, default=False)
tools_edit = Column(Boolean, default=False) tools_edit = Column(Boolean, default=False)
tools_achievement = Column(Boolean, default=False) tools_delete = Column(Boolean, default=False)
users_creation = Column(Boolean, default=False) users_creation = Column(Boolean, default=False)
users_edit = Column(Boolean, default=False) users_edit = Column(Boolean, default=False)
users_disabling = Column(Boolean, default=False) users_disabling = Column(Boolean, default=False)
@@ -31,6 +31,7 @@ class AccessLevel(Base):
available_own_toolbox = Column(Boolean, default=False) available_own_toolbox = Column(Boolean, default=False)
view_all_toolboxes = Column(Boolean, default=False) view_all_toolboxes = Column(Boolean, default=False)
view_requests = Column(Boolean, default=False) view_requests = Column(Boolean, default=False)
view_services = Column(Boolean, default=False)
access_level_view = Column(Boolean, default=False) access_level_view = Column(Boolean, default=False)
access_level_edit = Column(Boolean, default=False) access_level_edit = Column(Boolean, default=False)
manage_toolboxes = Column(Boolean, default=False) manage_toolboxes = Column(Boolean, default=False)
+1 -1
View File
@@ -9,7 +9,7 @@ class Category(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
title = Column(String, unique=True, index=True) title = Column(String, unique=True, index=True)
description = Column(Text) description = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+75
View File
@@ -0,0 +1,75 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from db import Base
import utils
class StocksRecords(Base):
__tablename__ = "stocks_records"
id = Column(Integer, primary_key=True, index=True)
action = Column(String, nullable=False)
source_toolbox_id = Column(
Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False
)
target_toolbox_id = Column(
Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=True
)
toolkit_id = Column(
Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False
)
init_user_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
accept_user_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
)
reason = Column(Text, nullable=False)
quantity = Column(Integer, nullable=False)
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)
edited_at = Column(DateTime, nullable=True)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def toDict(self):
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)
class ServicesRecords(Base):
__tablename__ = "services_records"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
details = Column(JSONB, nullable=False)
created_at = Column(DateTime, default=datetime.now)
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def toDict(self):
return utils.toDict(self)
async def save(self):
from db import CRUD
return await CRUD.create(self, refresh=True)
+2 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer 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
@@ -24,6 +24,7 @@ class Stock(Base):
) )
quantity = Column(Integer, nullable=False) quantity = Column(Integer, nullable=False)
price = Column(Float, nullable=False) price = Column(Float, nullable=False)
placement = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+4 -3
View File
@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from db import Base from db import Base
import utils import utils
@@ -9,10 +9,11 @@ class Toolbox(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
title = Column(String, unique=True, index=True) title = Column(String, unique=True, index=True)
description = Column(Text) description = Column(Text, nullable=True)
owner_id = Column( owner_id = Column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True Integer, ForeignKey("users.id", ondelete="CASCADE"), default=None, nullable=True
) )
monitoring = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+3 -4
View File
@@ -11,14 +11,13 @@ class Toolkit(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
title = Column(String, unique=True, index=True) title = Column(String, unique=True, index=True)
description = Column(Text) description = Column(Text, nullable=True)
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
) )
image = Column( image = Column(JSONB)
JSONB, default={"main": "images/tools/default.png", "additional": []}
)
quantity_min = Column(Integer, nullable=True) quantity_min = Column(Integer, nullable=True)
quantity_min_extra = Column(Integer, nullable=True) quantity_min_extra = Column(Integer, nullable=True)
external_link = Column(String, nullable=True) external_link = Column(String, nullable=True)
+16 -1
View File
@@ -1,5 +1,20 @@
from utils import logger, setLogLevel
def main(): def main():
print("Hello from tools-stock!") setLogLevel("WARNING")
logger.info("Приложение запущено")
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("Приложение завершено")
if __name__ == "__main__": if __name__ == "__main__":
+1
View File
@@ -7,6 +7,7 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"colorlog>=6.10.1", "colorlog>=6.10.1",
"passlib>=1.7.4", "passlib>=1.7.4",
"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.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+5 -3
View File
@@ -1,3 +1,5 @@
from my_loggers import * from .loggers import *
from for_DB import * from .for_DB import *
from password import * from .password import *
from .image import *
from .safe_filemane import *
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.
+60
View File
@@ -0,0 +1,60 @@
from utils.loggers import logger
# def saveImage(imageData, fileName: str):
# try:
# imageFormat = imageData.split(';')[0].split('/')[1]
# if imageFormat != 'png':
# logger.error(f"Неподдерживаемый формат изображения: {imageFormat}")
# return False
# imageData = imageData.split(';base64,')[1]
# with open(f"static/images/{fileName}", "wb") as f:
# f.write(base64.b64decode(imageData))
# logger.info(f"Изображение {fileName} успешно сохранено")
# return True
# except Exception as e:
# logger.error(f"Ошибка сохранения изображения: {str(e)}")
# return False
# UPLOAD_DIR = "uploads"
# os.makedirs(UPLOAD_DIR, exist_ok=True)
def saveImage(file_bytes, fileName: str):
from PIL import Image
import io
# Загружаем изображение через Pillow
try:
img = Image.open(io.BytesIO(file_bytes))
except Exception:
logger.error("Неподдерживаемый формат изображения")
return False
# Конвертация (если нужно)
if img.mode not in ("RGB", "RGBA", "P"):
img = img.convert("RGB")
# Сохранение в выбранный формат
try:
logger.info(f"Сохраняем изображение {fileName}")
img.save(fileName, "PNG")
except Exception as e:
logger.error(f"Ошибка сохранения изображения: {str(e)}")
return False
logger.info(f"Изображение {fileName} успешно сохранено")
return True
def deleteImage(fileName: str):
if fileName.endswith("default.png"):
return True
try:
import os
logger.info(f"Удаляем изображение {fileName}")
os.remove(f"static/images/{fileName}")
logger.info(f"Изображение {fileName} успешно удалено")
return True
except Exception as e:
logger.error(f"Ошибка удаления изображения: {str(e)}")
return False
+35
View File
@@ -0,0 +1,35 @@
import logging
import logging.config
import json
class SmartLogger(logging.Logger):
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=2):
# Увеличиваем stacklevel до 2, чтобы показать реального вызывающего
if isinstance(msg, (dict, list)):
msg = json.dumps(msg, indent=4, ensure_ascii=False)
super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
logging.setLoggerClass(SmartLogger)
logging.config.fileConfig("config/log.ini")
logger = logging.getLogger("toolbox")
def setLogLevel(level: str = ""):
match level:
case "DEBUG":
loggerLevel = logging.DEBUG
case "WARNING":
loggerLevel = logging.WARNING
case "ERROR":
loggerLevel = logging.ERROR
case _:
loggerLevel = logging.INFO
root_logger = logging.getLogger()
for handler in root_logger.handlers:
handler.setLevel(loggerLevel)
# Также меняем уровень самого логгера
logger.setLevel(loggerLevel)
-9
View File
@@ -1,9 +0,0 @@
import logging
import logging.config
logging.config.fileConfig("config/log.ini")
# loggers
logger = logging.getLogger("Tools Stock")
loggerDB = logging.getLogger("DB operations")
logger = logging.getLogger("UI operations")
+1 -1
View File
@@ -9,7 +9,7 @@ def pwd_hash(pwd_plant: str) -> str:
def pwd_verify(pwd_plant: str, pwd_hash: str) -> bool: def pwd_verify(pwd_plant: str, pwd_hash: str) -> bool:
from my_loggers import logger from utils.loggers import logger
try: try:
return pwd_context.verify(pwd_plant, pwd_hash) return pwd_context.verify(pwd_plant, pwd_hash)
+39
View File
@@ -0,0 +1,39 @@
import re
import time
# Простая транслитерация
TRANSLIT_MAP = {
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i',
'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '',
'э': 'e', 'ю': 'yu', 'я': 'ya'
}
# Заглавные буквы → в ту же латиницу, но без capital (ниже мы в lower() всё равно переводим)
TRANSLIT_MAP.update({k.upper(): v for k, v in TRANSLIT_MAP.items()})
def transliterate(text: str) -> str:
return ''.join(TRANSLIT_MAP.get(ch, ch) for ch in text)
def safeFilename(name: str) -> str:
# 1. Транслитерация кириллицы
name = transliterate(name)
# 2. Приводим к нижнему регистру
name = name.lower()
# 3. Заменяем всё, что не буква/цифра, на "_"
name = re.sub(r'[^a-z0-9]+', '_', name)
# 4. Убираем повторяющиеся "_"
name = re.sub(r'_+', '_', name).strip('_')
# 5. Ограничиваем длину
name = name[:80] or "file"
# 6. Добавляем таймштамп
timestamp = int(time.time() * 1000) # миллисекунды
return f"{name}_{timestamp}"
Generated
+60
View File
@@ -60,6 +60,64 @@ 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" }, { 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"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ 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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.1" version = "1.2.1"
@@ -97,6 +155,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "colorlog" }, { name = "colorlog" },
{ name = "passlib" }, { name = "passlib" },
{ name = "pillow" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
] ]
@@ -105,6 +164,7 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "colorlog", specifier = ">=6.10.1" }, { name = "colorlog", specifier = ">=6.10.1" },
{ name = "passlib", specifier = ">=1.7.4" }, { name = "passlib", specifier = ">=1.7.4" },
{ 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" },
] ]