diff --git a/.env b/.env new file mode 100644 index 0000000..9684b92 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DB_HOST=10.0.13.3 +DB_PORT=5432 +DB_NAME=tools_stock +DB_USER=tools_stock +DB_PASS=z7kWLkSKa6 \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md index f7b0fde..e209646 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # tool_stock -Учет хранения и перемещения инвентаря \ No newline at end of file +Учет хранения и перемещения инвентаря + diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..fa6510b --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,11 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +# DB +DB_HOST = os.environ.get("DB_HOST") +DB_PORT = os.environ.get("DB_PORT") +DB_NAME = os.environ.get("DB_NAME") +DB_USER = os.environ.get("DB_USER") +DB_PASS = os.environ.get("DB_PASS") diff --git a/config/log.ini b/config/log.ini new file mode 100644 index 0000000..49b8c67 --- /dev/null +++ b/config/log.ini @@ -0,0 +1,24 @@ +[loggers] +keys=root + +[handlers] +keys=logconsole + +[formatters] +keys=formatter +encoding=utf-8 + +[logger_root] +level=INFO +handlers=logconsole + +[formatter_formatter] +class=colorlog.ColoredFormatter +format=%(log_color)s%(asctime)s: [%(levelname)s] %(message)s [%(module)s.%(funcName)s():%(lineno)d] +datefmt=%Y-%m-%d %H:%M:%S + +[handler_logconsole] +class=colorlog.StreamHandler +level=INFO +args=(sys.stdout,) +formatter=formatter \ No newline at end of file diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..7462397 --- /dev/null +++ b/db/__init__.py @@ -0,0 +1,115 @@ +from sqlalchemy import delete, update +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker +from sqlalchemy.exc import InvalidRequestError +from sqlalchemy.pool import NullPool +import config +from utils import loggerDB + + +DATABASE_URL = f"postgresql+asyncpg://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}" + +engine = create_async_engine(DATABASE_URL, poolclass=NullPool) +SessionLocal = async_sessionmaker(engine) + +Base = declarative_base() + + +class CRUD: + + async def create(db_data, refresh: bool = False): + try: + is_lst = isinstance(db_data, list) + async with SessionLocal() as db: + if is_lst: + loggerDB.info(f"Создаю {len(db_data)} записей") + try: + db.add_all(db_data) + except InvalidRequestError: + for data in db_data: + await db.merge(data) + else: + loggerDB.info("Создаю запись") + db.add(db_data) + await db.commit() + if refresh: + if is_lst: + loggerDB.info(f"Обновляю {len(db_data)} записей") + for data in db_data: + await db.refresh(data) + else: + loggerDB.info("Обновляю запись") + await db.refresh(db_data) + loggerDB.info("Запись создана") + return db_data if refresh else None + except Exception as e: + loggerDB.error(f"Ошибка создания: {str(e)}", exc_info=True) + return None + + async def read(query, all: bool = False): + try: + async with SessionLocal() as db: + loggerDB.info(f"Чтение записей. Все: {all}") + results = await db.execute(query) + loggerDB.info(f"Чтение завершено") + return ( + results.unique().scalars().all() + if all + else results.unique().scalars().first() + ) + except Exception as e: + loggerDB.error(f"Ошибка чтения: {str(e)}", exc_info=True) + return None + + async def delete(db_data) -> bool: + def itemInfo(instance): + from sqlalchemy import inspect + + state = inspect(instance) + + if state.identity is None: + pKey = None + pValue = None + else: + mapper = state.mapper + pKey = mapper.primary_key[0].name + pValue = getattr(instance, pKey) + + return {"key": pKey, "value": pValue, "class": instance.__class__} + + async def deleteFromDB(data, db): + itemData = itemInfo(data) + query = delete(itemData["class"]).where( + getattr(itemData["class"], itemData["key"]) == itemData["value"] + ) + await db.execute(query) + + async with SessionLocal() as db: + try: + if isinstance(db_data, list): + loggerDB.info(f"Удаляю записей: {len(db_data)}") + for data in db_data: + await deleteFromDB(data, db) + else: + loggerDB.info("Удаляю запись") + await deleteFromDB(db_data, db) + await db.commit() + loggerDB.info("Запись удалена") + return True + except Exception as e: + await db.rollback() + loggerDB.error(f"Ошибка удаления: {str(e)}", exc_info=True) + return False + + async def update(db_data, id, **kwargs): + async with SessionLocal() as db: + try: + query = update(db_data).where(db_data.id == id).values(**kwargs) + item = await db.execute(query) + await db.commit() + loggerDB.info("Запись обновлена") + return item + except Exception as e: + await db.rollback() + loggerDB.error(f"Ошибка обновления: {str(e)}", exc_info=True) + return None diff --git a/db/handlers/access.py b/db/handlers/access.py new file mode 100644 index 0000000..17345e1 --- /dev/null +++ b/db/handlers/access.py @@ -0,0 +1,26 @@ +from sqlalchemy import select +from utils import logger +from db import CRUD +from db.schemas import AccessLevel + + +async def getAccessdata(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): + query = select(AccessLevel).where(AccessLevel.id == accessId) + accessData = await CRUD.read(query) + if not accessData: + logger.error("Уровень доступа не найден") + return {} + editedAccessData = await accessData.edit(**kwargs) + logger.info( + f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}" + ) + return editedAccessData.toDict() diff --git a/db/handlers/toolbox.py b/db/handlers/toolbox.py new file mode 100644 index 0000000..f89d5a8 --- /dev/null +++ b/db/handlers/toolbox.py @@ -0,0 +1,19 @@ +from utils import logger +from db import CRUD +from db.schemas import Toolbox +from sqlalchemy import select + + +async def addNewToolbox(toolboxData: dict): + title = toolboxData.get("title", None) + if not title: + logger.error("Не указано Назавание тулбокса") + return {} + query = select(Toolbox).where(Toolbox.title == title) + toolbox = await CRUD.read(query) + if toolbox: + logger.error("Тулбокс с таким названием уже существует") + return {} + newToolbox = await Toolbox(**toolboxData).save() + logger.info(f"Тулбокс {newToolbox.title} успешно создан") + return newToolbox.toDict() diff --git a/db/handlers/user.py b/db/handlers/user.py new file mode 100644 index 0000000..0a3717a --- /dev/null +++ b/db/handlers/user.py @@ -0,0 +1,81 @@ +from sqlalchemy import or_, select +from db import CRUD +from db.handlers.toolbox import addNewToolbox +from db.schemas import User +from utils import logger, pwd_hash + + +async def addNewUser(userData: dict) -> dict: + login = userData.get("login", None) + if not login: + logger.error("Не указан логин") + return {} + userName = userData.get("username") + if not userName: + logger.error("Не указано имя пользователя") + return {} + query = select(User).where(or_(User.login == login, User.username == userName)) + user = await CRUD.read(query) + if user: + logger.error("Пользователь с таким логином или именем уже существует") + return {} + if "access_level_id" not in userData: + logger.error("Не указан уровень доступа") + return {} + if "password" not in userData: + logger.error("Не указан пароль") + return {} + userData["hashed_password"] = pwd_hash(userData.pop("password")) + newUser = await User(**userData).save() + if not newUser: + logger.error("Ошибка сохранения пользователя") + return {} + logger.info(f"Пользователь {newUser.username} успешно добавлен, id: {newUser.id}") + if newUser.available_own_toolbox: + newToolboxData = { + "title": f"Тулбокс {newUser.username}", + "description": f"Оборудование, полученное сотрудником {newUser.username}, под личную материальную ответственность", + "owner_id": newUser.id, + } + newToolbox = await addNewToolbox(newToolboxData) + logger.info( + f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}" + ) + return newUser.toDict() + + +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 = { + "title": f"Тулбокс {editedUser.username}", + "description": f"Оборудование, полученное сотрудником {editedUser.username}, под личную материальную ответственность", + "owner_id": editedUser.id, + } + newToolbox = await addNewToolbox(newToolboxData) + logger.info( + f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" + ) + return editedUser.toDict() diff --git a/db/schemas/__init__.py b/db/schemas/__init__.py new file mode 100644 index 0000000..2eb570f --- /dev/null +++ b/db/schemas/__init__.py @@ -0,0 +1,6 @@ +from user import * +from access import * +from toolkit import * +from categories import * +from db.schemas.toolbox import * +from stock import * diff --git a/db/schemas/access.py b/db/schemas/access.py new file mode 100644 index 0000000..726d2bd --- /dev/null +++ b/db/schemas/access.py @@ -0,0 +1,53 @@ +from datetime import datetime +from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text +from db import Base +import utils + + +class AccessLevel(Base): + __tablename__ = "access_level" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, unique=True, index=True) + description = Column(Text) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + # permissions + receiving_edit = Column(Boolean, default=False) + refund_request_edit = Column(Boolean, default=False) + refund_request_confirm = Column(Boolean, default=False) + debit_request_edit = Column(Boolean, default=False) + debit_request_confirm = Column(Boolean, default=False) + tools_creation = Column(Boolean, default=False) + tools_registration = Column(Boolean, default=False) + tools_registration_edit = Column(Boolean, default=False) + tools_edit = Column(Boolean, default=False) + tools_achievement = Column(Boolean, default=False) + users_creation = Column(Boolean, default=False) + users_edit = Column(Boolean, default=False) + users_disabling = Column(Boolean, default=False) + users_view = Column(Boolean, default=False) + available_own_toolbox = Column(Boolean, default=False) + view_all_toolboxes = Column(Boolean, default=False) + view_requests = Column(Boolean, default=False) + access_level_view = Column(Boolean, default=False) + access_level_edit = Column(Boolean, default=False) + manage_toolboxes = Column(Boolean, default=False) + + 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(AccessLevel, self.id, **kwargs) diff --git a/db/schemas/categories.py b/db/schemas/categories.py new file mode 100644 index 0000000..7306e99 --- /dev/null +++ b/db/schemas/categories.py @@ -0,0 +1,31 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String, Text +from db import Base +import utils + + +class Category(Base): + __tablename__ = "categories" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, unique=True, index=True) + description = Column(Text) + created_at = Column(DateTime, default=datetime.now) + 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(id: int, **kwargs): + from db import CRUD + + return await CRUD.update(Category, id, **kwargs) diff --git a/db/schemas/stock.py b/db/schemas/stock.py new file mode 100644 index 0000000..96cfbc6 --- /dev/null +++ b/db/schemas/stock.py @@ -0,0 +1,45 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from db import Base +import utils + + +class Stock(Base): + __tablename__ = "stocks" + + id = Column(Integer, primary_key=True, index=True) + toolkit_id = Column( + Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False + ) + toolkit_data = relationship( + "Toolkit", cascade="all, delete-orphan", lazy="joined", uselist=False + ) + toolbox_id = Column( + Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False + ) + toolbox_data = relationship( + "Toolbox", cascade="all, delete-orphan", lazy="joined", uselist=False + ) + quantity = Column(Integer, nullable=False) + price = Column(Float, nullable=False) + created_at = Column(DateTime, default=datetime.now) + 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(Stock, self.id, **kwargs) diff --git a/db/schemas/toolbox.py b/db/schemas/toolbox.py new file mode 100644 index 0000000..f04aac5 --- /dev/null +++ b/db/schemas/toolbox.py @@ -0,0 +1,34 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from db import Base +import utils + + +class Toolbox(Base): + __tablename__ = "toolboxes" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, unique=True, index=True) + description = Column(Text) + owner_id = Column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True + ) + created_at = Column(DateTime, default=datetime.now) + 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(id: int, **kwargs): + from db import CRUD + + return await CRUD.update(Toolbox, id, **kwargs) diff --git a/db/schemas/toolkit.py b/db/schemas/toolkit.py new file mode 100644 index 0000000..cd7b849 --- /dev/null +++ b/db/schemas/toolkit.py @@ -0,0 +1,45 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import relationship +from db import Base +import utils + + +class Toolkit(Base): + __tablename__ = "toolkits" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, unique=True, index=True) + description = Column(Text) + category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE")) + category_data = relationship( + "Category", cascade="all, delete-orphan", lazy="joined", uselist=False + ) + image = Column( + JSONB, default={"main": "images/tools/default.png", "additional": []} + ) + quantity_min = Column(Integer, nullable=True) + quantity_min_extra = Column(Integer, nullable=True) + external_link = Column(String, nullable=True) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + refilled_at = Column(DateTime, default=datetime.now) + moved_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) + + async def edit(id: int, **kwargs): + from db import CRUD + + return await CRUD.update(Toolkit, id, **kwargs) diff --git a/db/schemas/user.py b/db/schemas/user.py new file mode 100644 index 0000000..ee7203b --- /dev/null +++ b/db/schemas/user.py @@ -0,0 +1,35 @@ +from datetime import datetime +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from db import Base +import utils + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + login = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True) + photo = Column(String, default="images/users/default.png") + hashed_password = Column(String) + access_level_id = Column(Integer, ForeignKey("access_level.id", ondelete="CASCADE")) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.now) + 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) -> "User": + from db import CRUD + + return await CRUD.create(self, refresh=True) + + async def edit(self, **kwargs) -> "User": + from db import CRUD + + return await CRUD.update(User, self.id, **kwargs) diff --git a/main.py b/main.py new file mode 100644 index 0000000..fb04cd0 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from tools-stock!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..30a3d20 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "tools-stock" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "colorlog>=6.10.1", + "passlib>=1.7.4", + "python-dotenv>=1.2.1", + "sqlalchemy>=2.0.44", +] diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..90a4922 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,3 @@ +from my_loggers import * +from for_DB import * +from password import * diff --git a/utils/for_DB.py b/utils/for_DB.py new file mode 100644 index 0000000..ba631e4 --- /dev/null +++ b/utils/for_DB.py @@ -0,0 +1,13 @@ +def toDict(data) -> dict: + return { + c.name: ( + ( + getattr(data, c.name) + if not c.name.endswith("_data") + else getattr(data, c.name).toDict() + ) + if not c.name.endswith("_at") + else getattr(data, c.name).strftime("%Y-%m-%d %H:%M:%S") + ) + for c in data.__table__.columns + } diff --git a/utils/my_loggers.py b/utils/my_loggers.py new file mode 100644 index 0000000..4f61700 --- /dev/null +++ b/utils/my_loggers.py @@ -0,0 +1,9 @@ +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") diff --git a/utils/password.py b/utils/password.py new file mode 100644 index 0000000..19c4165 --- /dev/null +++ b/utils/password.py @@ -0,0 +1,18 @@ +from passlib.context import CryptContext + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def pwd_hash(pwd_plant: str) -> str: + return pwd_context.encrypt(pwd_plant) + + +def pwd_verify(pwd_plant: str, pwd_hash: str) -> bool: + from my_loggers import logger + + try: + return pwd_context.verify(pwd_plant, pwd_hash) + except Exception as e: + logger.error(f"Password verification error: {str(e)}") + return False diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..616a623 --- /dev/null +++ b/uv.lock @@ -0,0 +1,119 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "tools-stock" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "colorlog" }, + { name = "passlib" }, + { name = "python-dotenv" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "colorlog", specifier = ">=6.10.1" }, + { name = "passlib", specifier = ">=1.7.4" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "sqlalchemy", specifier = ">=2.0.44" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]