создана часть бекенда
This commit is contained in:
+21
-3
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
+146
-18
@@ -2,25 +2,153 @@ 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:
|
||||||
query = select(AccessLevel).where(AccessLevel.id == accessId)
|
async def add(**kwargs):
|
||||||
accessData = await CRUD.read(query)
|
title = kwargs.get("title", None)
|
||||||
if not accessData:
|
if not title:
|
||||||
logger.error("Уровень доступа не найден")
|
logger.error("Не указано название уровня доступа")
|
||||||
return {}
|
return {}
|
||||||
return accessData.toDict()
|
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)
|
||||||
|
accessData = await CRUD.read(query)
|
||||||
|
if not accessData:
|
||||||
|
logger.error("Уровень доступа не найден")
|
||||||
|
return {}
|
||||||
|
return accessData.toDict()
|
||||||
|
|
||||||
async def editAccessData(accessId: int, **kwargs):
|
async def edit(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 {}
|
||||||
editedAccessData = await accessData.edit(**kwargs)
|
try:
|
||||||
logger.info(
|
user_id = kwargs.pop("user_id", None)
|
||||||
f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}"
|
editedAccessData = await accessData.edit(**kwargs)
|
||||||
)
|
await ServiceRecordsHandler.add(
|
||||||
return editedAccessData.toDict()
|
user_id, {"Обновлен уровень доступа": editedAccessData.toDict()}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка обновления уровня доступа: {str(e)}")
|
||||||
|
return {}
|
||||||
|
logger.info(
|
||||||
|
f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+107
-13
@@ -2,18 +2,112 @@ 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:
|
||||||
title = toolboxData.get("title", None)
|
async def add(toolboxData: dict, user_id: int = None):
|
||||||
if not title:
|
title = toolboxData.get("title", None)
|
||||||
logger.error("Не указано Назавание тулбокса")
|
if not title:
|
||||||
return {}
|
logger.error("Не указано Назавание тулбокса")
|
||||||
query = select(Toolbox).where(Toolbox.title == title)
|
return {}
|
||||||
toolbox = await CRUD.read(query)
|
query = select(Toolbox).where(Toolbox.title == title)
|
||||||
if toolbox:
|
toolbox = await CRUD.read(query)
|
||||||
logger.error("Тулбокс с таким названием уже существует")
|
if toolbox:
|
||||||
return {}
|
logger.error("Тулбокс с таким названием уже существует")
|
||||||
newToolbox = await Toolbox(**toolboxData).save()
|
return {}
|
||||||
logger.info(f"Тулбокс {newToolbox.title} успешно создан")
|
|
||||||
return newToolbox.toDict()
|
try:
|
||||||
|
logger.info(f"Создание тулбокса {title}")
|
||||||
|
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} успешно создан")
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
+209
-68
@@ -1,81 +1,222 @@
|
|||||||
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 = userData.get("login", None)
|
login = safeFilename(login)
|
||||||
if not login:
|
fileName = f"users/{login}.png"
|
||||||
logger.error("Не указан логин")
|
if not saveImage(imageData, fileName):
|
||||||
return {}
|
return None
|
||||||
userName = userData.get("username")
|
return fileName
|
||||||
if not userName:
|
|
||||||
logger.error("Не указано имя пользователя")
|
|
||||||
return {}
|
class UserHandler:
|
||||||
query = select(User).where(or_(User.login == login, User.username == userName))
|
async def add(userData: dict, user_id: int = None) -> dict:
|
||||||
user = await CRUD.read(query)
|
login = userData.get("login", None)
|
||||||
if user:
|
if not login:
|
||||||
logger.error("Пользователь с таким логином или именем уже существует")
|
logger.error("Не указан логин")
|
||||||
return {}
|
return {}
|
||||||
if "access_level_id" not in userData:
|
userName = userData.get("username", None)
|
||||||
logger.error("Не указан уровень доступа")
|
if not userName:
|
||||||
return {}
|
logger.error("Не указано имя пользователя")
|
||||||
if "password" not in userData:
|
return {}
|
||||||
logger.error("Не указан пароль")
|
query = select(User).where(or_(User.login == login, User.username == userName))
|
||||||
return {}
|
user = await CRUD.read(query)
|
||||||
userData["hashed_password"] = pwd_hash(userData.pop("password"))
|
if user:
|
||||||
newUser = await User(**userData).save()
|
logger.error("Пользователь с таким логином или именем уже существует")
|
||||||
if not newUser:
|
return {}
|
||||||
logger.error("Ошибка сохранения пользователя")
|
if "access_level_id" not in userData:
|
||||||
return {}
|
logger.error("Не указан уровень доступа")
|
||||||
logger.info(f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}")
|
return {}
|
||||||
if newUser.available_own_toolbox:
|
if "password" not in userData:
|
||||||
newToolboxData = {
|
logger.error("Не указан пароль")
|
||||||
"title": f"Тулбокс {newUser.username}",
|
return {}
|
||||||
"description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность",
|
userData["hashed_password"] = pwd_hash(userData.pop("password"))
|
||||||
"owner_id": newUser.id,
|
if "photo" in userData:
|
||||||
}
|
imageData = userData.pop("photo")
|
||||||
newToolbox = await addNewToolbox(newToolboxData)
|
photoFile = handleUserPhoto(imageData, login)
|
||||||
|
if photoFile:
|
||||||
|
userData["photo"] = photoFile
|
||||||
|
try:
|
||||||
|
newUser = await User(**userData).save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка сохранения пользователя: {str(e)}")
|
||||||
|
return {}
|
||||||
|
if not newUser:
|
||||||
|
logger.error("Пользователь не сохранен")
|
||||||
|
return {}
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
|
f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}"
|
||||||
)
|
)
|
||||||
return newUser.toDict()
|
if newUser.available_own_toolbox:
|
||||||
|
|
||||||
|
|
||||||
async def editUser(userData: dict) -> dict:
|
|
||||||
id = userData.get("id", None)
|
|
||||||
if not id:
|
|
||||||
logger.error("Не указан id пользователя")
|
|
||||||
return {}
|
|
||||||
query = select(User).where(User.id == id)
|
|
||||||
user = await CRUD.read(query)
|
|
||||||
if not user:
|
|
||||||
logger.error("Пользователь с таким id не найден")
|
|
||||||
return {}
|
|
||||||
changedUserData = userData.get("changedUserData", {})
|
|
||||||
if len(changedUserData.keys()) == 0:
|
|
||||||
logger.error("Не указаны изменяемые данные")
|
|
||||||
return {}
|
|
||||||
if "password" in changedUserData:
|
|
||||||
userData["hashed_password"] = pwd_hash(changedUserData.pop("password"))
|
|
||||||
editedUser = await user.edit(**changedUserData)
|
|
||||||
if not editedUser:
|
|
||||||
logger.error("Ошибка обновления пользователя")
|
|
||||||
return {}
|
|
||||||
logger.info(
|
|
||||||
f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {changedUserData.keys()}"
|
|
||||||
)
|
|
||||||
if not user.available_own_toolbox:
|
|
||||||
if editedUser.available_own_toolbox:
|
|
||||||
newToolboxData = {
|
newToolboxData = {
|
||||||
"title": f"Тулбокс {editedUser.username}",
|
"title": f"Тулбокс {newUser.username}",
|
||||||
"description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность",
|
"description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность",
|
||||||
"owner_id": editedUser.id,
|
"owner_id": newUser.id,
|
||||||
}
|
}
|
||||||
newToolbox = await addNewToolbox(newToolboxData)
|
newToolbox = await addNewToolbox(newToolboxData)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
|
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
|
||||||
)
|
)
|
||||||
return editedUser.toDict()
|
await ServiceRecordsHandler.add(
|
||||||
|
user_id, {"Добавлен пользователь": newUser.toDict()}
|
||||||
|
)
|
||||||
|
return newUser.toDict()
|
||||||
|
|
||||||
|
async def edit(userData: dict, user_id: int = None) -> dict:
|
||||||
|
id = userData.get("id", None)
|
||||||
|
if not id:
|
||||||
|
logger.error("Не указан id пользователя")
|
||||||
|
return {}
|
||||||
|
query = select(User).where(User.id == id)
|
||||||
|
user = await CRUD.read(query)
|
||||||
|
if not user:
|
||||||
|
logger.error("Пользователь с таким id не найден")
|
||||||
|
return {}
|
||||||
|
changedUserData = userData.get("changedUserData", {})
|
||||||
|
if len(changedUserData.keys()) == 0:
|
||||||
|
logger.error("Не указаны изменяемые данные")
|
||||||
|
return {}
|
||||||
|
if "password" in changedUserData:
|
||||||
|
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)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка обновления пользователя: {str(e)}")
|
||||||
|
return {}
|
||||||
|
if not editedUser:
|
||||||
|
logger.error("Ошибка обновления пользователя")
|
||||||
|
return {}
|
||||||
|
logger.info(
|
||||||
|
f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {changedUserData.keys()}"
|
||||||
|
)
|
||||||
|
if not user.available_own_toolbox:
|
||||||
|
if editedUser.available_own_toolbox:
|
||||||
|
newToolboxData = {
|
||||||
|
"title": f"Тулбокс {editedUser.username}",
|
||||||
|
"description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность",
|
||||||
|
"owner_id": editedUser.id,
|
||||||
|
}
|
||||||
|
newToolbox = await addNewToolbox(newToolboxData)
|
||||||
|
logger.info(
|
||||||
|
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
|
||||||
|
)
|
||||||
|
await ServiceRecordsHandler.add(
|
||||||
|
user_id, {"Изменен пользователь": 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
@@ -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",
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
|
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__":
|
||||||
main()
|
main()
|
||||||
@@ -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
@@ -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.
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -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" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user