├── droos_bot ├── gsheet │ ├── __init__.py │ └── spreadsheet.py ├── utils │ ├── __init__.py │ ├── commands.py │ ├── modules_loader.py │ ├── filters.py │ ├── analytics.py │ ├── telegram.py │ └── keyboards.py ├── db │ ├── models │ │ ├── __init__.py │ │ ├── series.py │ │ ├── lecture.py │ │ └── chat.py │ ├── base.py │ ├── __init__.py │ ├── session.py │ └── curd.py ├── __main__.py ├── modules │ ├── __init__.py │ ├── update.py │ ├── restart.py │ ├── start.py │ ├── broadcast.py │ ├── stats.py │ ├── search.py │ ├── droos.py │ └── feedback_and_files.py ├── bot.py └── __init__.py ├── .github └── FUNDING.yml ├── mise.toml ├── docker-compose.yml ├── Dockerfile ├── config.example.json ├── renovate.json5 ├── .dockerignore ├── LICENSE ├── .gitignore ├── pyproject.toml ├── .pre-commit-config.yaml ├── README.md └── uv.lock /droos_bot/gsheet/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /droos_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /droos_bot/db/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /droos_bot/db/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /droos_bot/__main__.py: -------------------------------------------------------------------------------- 1 | """Bot Entry Point.""" 2 | 3 | from droos_bot.bot import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: yshalsager 2 | patreon: XiaomiFirmwareUpdater 3 | liberapay: yshalsager 4 | custom: ['https://paypal.me/yshalsager'] 5 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | uv = 'latest' 3 | python = '3.14.2' 4 | 5 | [tasks.default] 6 | run = 'python -m droos_bot' 7 | 8 | [tasks.debug] 9 | run = 'jurigged -v -m droos_bot' 10 | -------------------------------------------------------------------------------- /droos_bot/modules/__init__.py: -------------------------------------------------------------------------------- 1 | """Modules loader.""" 2 | 3 | from pathlib import Path 4 | 5 | from droos_bot.utils.modules_loader import get_modules 6 | 7 | ALL_MODULES: filter = get_modules(Path(__file__)) 8 | -------------------------------------------------------------------------------- /droos_bot/db/__init__.py: -------------------------------------------------------------------------------- 1 | """Database package entry.""" 2 | 3 | from droos_bot.db.models.chat import Chat 4 | from droos_bot.db.models.lecture import Lecture 5 | from droos_bot.db.models.series import Series 6 | 7 | __all__ = ["Chat", "Lecture", "Series"] 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | droos_bot: 3 | build: 4 | context: ./ 5 | dockerfile: Dockerfile 6 | command: bash -c "cd app && python3 -m droos_bot" 7 | environment: 8 | - PUID=1000 9 | - PGID=1000 10 | - TZ=Africa/Cairo 11 | - /etc/localtime:/etc/localtime:ro 12 | restart: unless-stopped 13 | volumes: 14 | - "./:/app/app" 15 | -------------------------------------------------------------------------------- /droos_bot/db/models/series.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import VARCHAR, Column, Integer 2 | 3 | from droos_bot.db.base import Base 4 | 5 | 6 | class Series(Base): 7 | __tablename__ = "series" 8 | id: str = Column(VARCHAR, primary_key=True, nullable=False) 9 | requests: int = Column(Integer, nullable=False, default=0) 10 | 11 | def __repr__(self) -> str: 12 | return f"" 13 | -------------------------------------------------------------------------------- /droos_bot/db/models/lecture.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import VARCHAR, Column, Integer 2 | 3 | from droos_bot.db.base import Base 4 | 5 | 6 | class Lecture(Base): 7 | __tablename__ = "lectures" 8 | id: str = Column(VARCHAR, primary_key=True, nullable=False) 9 | requests: int = Column(Integer, nullable=False, default=0) 10 | 11 | def __repr__(self) -> str: 12 | return f"" 13 | -------------------------------------------------------------------------------- /droos_bot/bot.py: -------------------------------------------------------------------------------- 1 | """Telegram Bot.""" 2 | 3 | from droos_bot import application 4 | from droos_bot.modules import ALL_MODULES 5 | from droos_bot.utils.modules_loader import load_modules 6 | 7 | 8 | def main() -> None: 9 | """Run bot.""" 10 | # Load all modules in modules list 11 | load_modules(ALL_MODULES, __package__) 12 | # Start the Bot 13 | application.run_polling() 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /droos_bot/utils/commands.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from subprocess import PIPE, Popen 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | def run_command(command: str) -> str: 8 | with Popen( # noqa: S602 9 | command, 10 | stdout=PIPE, 11 | bufsize=1, 12 | universal_newlines=True, 13 | shell=True, 14 | ) as p: 15 | assert p.stdout is not None 16 | output = p.stdout.read() 17 | logger.info(f"Ran command: {command}\nOutput: {output}") 18 | return output 19 | -------------------------------------------------------------------------------- /droos_bot/db/session.py: -------------------------------------------------------------------------------- 1 | """Database initialization.""" 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import scoped_session, sessionmaker 5 | 6 | from droos_bot import PARENT_DIR 7 | from droos_bot.db.base import Base 8 | 9 | db_connection_string = f"sqlite:///{PARENT_DIR}/droos_bot.db" 10 | engine = create_engine(db_connection_string, connect_args={"check_same_thread": False}) 11 | 12 | Base.metadata.create_all(bind=engine) 13 | 14 | session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.14-slim 2 | ARG DEBIAN_FRONTEND=noninteractive 3 | 4 | RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y git > /dev/null \ 5 | && apt-get clean && rm -rf /var/lib/apt/lists/* 6 | 7 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/ 8 | WORKDIR /code 9 | COPY pyproject.toml uv.lock /code/ 10 | RUN uv sync --frozen --no-cache 11 | ENV PATH="/code/.venv/bin:$PATH" 12 | 13 | RUN useradd -m appuser 14 | USER appuser 15 | 16 | WORKDIR /code/app 17 | RUN git config --global pull.rebase true 18 | #COPY --chown=appuser:appuser . . 19 | #CMD ["python3", "-m", "droos_bot"] 20 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "tg_bot_token": "0000000000:aaaaaaaaaaaaaaaaaaaa", 3 | "tg_bot_admins": [ 4 | 100000, 5 | 200000, 6 | 300000 7 | ], 8 | "tg_feedback_chat_id": -10000000000, 9 | "sheet_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 10 | "sheet_name": "sheet", 11 | "data_columns": { 12 | "series": "السلاسل العلمية" 13 | }, 14 | "lecture_components": { 15 | "book": "📕 الكتاب", 16 | "main": "📝 المحاور", 17 | "video": "🎞 فيديو", 18 | "voice": "🎧 صوتي", 19 | "text": "📄 تفريغ", 20 | "summary": "📎 ملخص" 21 | }, 22 | "hide": [], 23 | "disable": [ 24 | "إرسال مواد" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /droos_bot/db/models/chat.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import BIGINT, INT, VARCHAR, Column 2 | 3 | from droos_bot.db.base import Base 4 | 5 | 6 | class Chat(Base): 7 | __tablename__ = "chats" 8 | id: int = Column(INT(), primary_key=True, autoincrement=True, nullable=False) 9 | user_id: int = Column(BIGINT(), unique=True, nullable=False) 10 | user_name: str = Column(VARCHAR(), nullable=False) 11 | type: int = Column(INT(), nullable=False) # 0=user, 1=group, 2=channel 12 | usage_times: int = Column(INT(), nullable=False, default=0) 13 | 14 | def __repr__(self) -> str: 15 | return f"" 16 | -------------------------------------------------------------------------------- /droos_bot/utils/modules_loader.py: -------------------------------------------------------------------------------- 1 | """Bot modules dynamic loader.""" 2 | 3 | import logging 4 | from importlib import import_module 5 | from pathlib import Path 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def get_modules(modules_path: Path) -> filter: 11 | """Return all modules available in modules directory.""" 12 | return filter( 13 | lambda x: x.name != "__init__.py" and x.suffix == ".py" and x.is_file(), 14 | modules_path.parent.glob("*.py"), 15 | ) 16 | 17 | 18 | def load_modules(modules: filter, directory: str) -> None: 19 | """Load all modules in modules list.""" 20 | loaded_modules = [] 21 | for module in modules: 22 | module_name = f"{directory}.modules.{module.stem}" 23 | import_module(module_name) 24 | loaded_modules.append(module.stem) 25 | logger.info(f"Loaded modules: {loaded_modules!s}") 26 | -------------------------------------------------------------------------------- /droos_bot/utils/filters.py: -------------------------------------------------------------------------------- 1 | """Bot custom filters.""" 2 | 3 | from telegram import Message 4 | from telegram.ext.filters import MessageFilter 5 | 6 | from droos_bot import TG_BOT_ADMINS 7 | 8 | 9 | class FilterBotAdmin(MessageFilter): 10 | def filter(self, message: Message) -> bool: 11 | return bool(message.from_user and message.from_user.id in TG_BOT_ADMINS) 12 | 13 | 14 | class FeedbackMessageFilter(MessageFilter): 15 | def __init__(self, feedback_chat: int, bot_id: str) -> None: 16 | super().__init__() 17 | self.feedback_chat = feedback_chat 18 | self.bot_id = int(bot_id) 19 | 20 | def filter(self, message: Message) -> bool: 21 | return bool( 22 | message.reply_to_message 23 | and message.reply_to_message.chat_id == self.feedback_chat 24 | and message.reply_to_message.from_user 25 | and message.reply_to_message.from_user.id == self.bot_id 26 | ) 27 | -------------------------------------------------------------------------------- /droos_bot/modules/update.py: -------------------------------------------------------------------------------- 1 | """Bot update module.""" 2 | 3 | from telegram import Update 4 | from telegram.ext import CommandHandler, ContextTypes 5 | 6 | from droos_bot import application 7 | from droos_bot.modules.restart import restart 8 | from droos_bot.utils.commands import run_command 9 | from droos_bot.utils.filters import FilterBotAdmin 10 | 11 | 12 | async def update_(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: 13 | """Update the bot then restart.""" 14 | assert update.effective_message is not None 15 | assert update.effective_chat is not None 16 | git_output = run_command("git fetch origin master && git reset --hard origin/master") 17 | if git_output: 18 | await update.effective_message.reply_text( 19 | f"
{git_output}
", 20 | reply_to_message_id=update.effective_message.message_id, 21 | ) 22 | await restart(update, _) 23 | 24 | 25 | application.add_handler(CommandHandler("update", update_, FilterBotAdmin())) 26 | -------------------------------------------------------------------------------- /droos_bot/modules/restart.py: -------------------------------------------------------------------------------- 1 | """Bot restart module.""" 2 | 3 | import json 4 | from os import execl 5 | from pathlib import Path 6 | from sys import executable 7 | 8 | from telegram import Update 9 | from telegram.ext import CommandHandler, ContextTypes, filters 10 | 11 | from droos_bot import PARENT_DIR, application 12 | from droos_bot.utils.filters import FilterBotAdmin 13 | 14 | 15 | async def restart(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: 16 | """Restarts the bot.""" 17 | restart_message = await update.message.reply_text( 18 | "Restarting, please wait...", 19 | ) 20 | chat_info = {"chat": restart_message.chat_id, "message": restart_message.message_id} 21 | Path(f"{PARENT_DIR}/restart.json").write_text(json.dumps(chat_info), encoding="utf-8") 22 | execl(executable, executable, "-m", __package__.split(".")[0]) # noqa: S606 23 | 24 | 25 | application.add_handler( 26 | CommandHandler("restart", restart, filters.ChatType.PRIVATE & FilterBotAdmin()) 27 | ) 28 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ':enablePreCommit', 6 | ':rebaseStalePrs', 7 | ], 8 | labels: [ 9 | 'dependencies', 10 | ], 11 | schedule: [ 12 | 'before 5am on saturday', 13 | ], 14 | lockFileMaintenance: { 15 | enabled: true, 16 | automerge: true, 17 | schedule: [ 18 | 'before 5am on saturday', 19 | ], 20 | }, 21 | packageRules: [ 22 | { 23 | matchUpdateTypes: [ 24 | 'minor', 25 | 'patch', 26 | 'pin', 27 | 'digest', 28 | ], 29 | automerge: true, 30 | }, 31 | ], 32 | customManagers: [ 33 | { 34 | customType: 'regex', 35 | fileMatch: [ 36 | '\\.pre-commit-config\\.yaml', 37 | ], 38 | matchStrings: [ 39 | '(?[^\'" ]+)==(?[^\'" ,\\s]+)', 40 | ], 41 | datasourceTemplate: 'pypi', 42 | versioningTemplate: 'pep440', 43 | }, 44 | ], 45 | pip_requirements: { 46 | enabled: false, 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | share/python-wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Environments 56 | .env 57 | .venv 58 | env/ 59 | venv/ 60 | ENV/ 61 | env.bak/ 62 | venv.bak/ 63 | 64 | # IDE 65 | .idea/ 66 | .vscode/ 67 | 68 | # Bot files 69 | config.json.example 70 | last_run.log* 71 | tmp_* 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Youssif Shaaban Alsager (yshalsager) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /droos_bot/modules/start.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CommandHandler, ContextTypes, MessageHandler, filters 3 | 4 | from droos_bot import application 5 | from droos_bot.utils.analytics import add_new_chat_to_db 6 | from droos_bot.utils.keyboards import main_keyboard 7 | 8 | 9 | @add_new_chat_to_db 10 | async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 11 | assert update.effective_chat is not None 12 | welcome_text = ( 13 | f" مرحبا بك يا {update.effective_chat.full_name or update.effective_chat.title}" 14 | f"\n بإمكانك استخدام البوت من خلال الضغط على الأزرار الظاهرة بالأسفل" 15 | ) 16 | context.user_data["path"] = [] 17 | await update.message.reply_text(welcome_text, reply_markup=main_keyboard) 18 | 19 | 20 | application.add_handler(CommandHandler("start", start_handler, filters.ChatType.PRIVATE)) 21 | application.add_handler(CommandHandler("help", start_handler, filters.ChatType.PRIVATE)) 22 | application.add_handler( 23 | MessageHandler( 24 | filters.ChatType.PRIVATE & filters.Regex("^(القائمة الرئيسية 🏠)$"), start_handler 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | share/python-wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Environments 56 | .env 57 | .venv 58 | env/ 59 | venv/ 60 | ENV/ 61 | env.bak/ 62 | venv.bak/ 63 | .mise.local.toml 64 | 65 | # IDE 66 | .idea/ 67 | .vscode/ 68 | 69 | # Bot files 70 | config*.json 71 | *.pickle 72 | last_run.log* 73 | *.session 74 | service_account*.json 75 | tmp_* 76 | *.db 77 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "droos-bot" 3 | version = "1.0.0" 4 | description = "" 5 | authors = [ 6 | { name = "yshalsager", email = "contact@yshalsager.com" }, 7 | ] 8 | license = { text = "MIT" } 9 | readme = "README.md" 10 | requires-python = ">=3.13" 11 | dependencies = [ 12 | "gspread>=5.12.4", 13 | "gspread-pandas>=3.3.0", 14 | "pyarabic>=0.6.15", 15 | "python-telegram-bot>=21.4", 16 | "SQLAlchemy>=2.0.31" 17 | ] 18 | 19 | [dependency-groups] 20 | dev = [ 21 | "ruff>=0.8.1", 22 | "pre-commit>=4.0.0", 23 | "jurigged>=0.6.0" 24 | ] 25 | 26 | [tool.ruff] # https://github.com/charliermarsh/ruff 27 | fix = true 28 | src = ["droos_bot"] 29 | target-version = "py313" 30 | line-length = 100 31 | 32 | [tool.ruff.format] 33 | quote-style = "double" 34 | line-ending = "lf" 35 | 36 | [tool.ruff.lint] 37 | select = ["A", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "ERA", "F", "G", "I", "INP", "ISC", "N", "NPY", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "T10", "T20", "TID", "UP", "W", "YTT"] 38 | ignore = ["E501", "S307", "RET504", "S101", "D211", "D213", "ERA001", "G004", "D100", "D101", "D102", "D103", "D104", "D105", "D104", "D107", "D203", "ISC001"] 39 | unfixable = ["ERA001", "F401", "F841", "T201", "T203"] 40 | 41 | [tool.mypy] 42 | files = ["droos_bot"] 43 | ignore_missing_imports = true 44 | disallow_untyped_defs = true 45 | #disallow_any_unimported = true 46 | no_implicit_optional = true 47 | check_untyped_defs = true 48 | warn_return_any = true 49 | show_error_codes = true 50 | warn_unused_ignores = true 51 | disallow_incomplete_defs = true 52 | disallow_untyped_decorators = true 53 | plugins = ["sqlalchemy.ext.mypy.plugin"] 54 | -------------------------------------------------------------------------------- /droos_bot/utils/analytics.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from functools import wraps 3 | from typing import Any, TypeVar, cast 4 | 5 | from telegram import Update 6 | from telegram.ext import CallbackContext 7 | 8 | from droos_bot.db.curd import ( 9 | add_chat_to_db, 10 | increment_lecture_requests, 11 | increment_series_requests, 12 | increment_usage, 13 | ) 14 | from droos_bot.utils.telegram import get_chat_type 15 | 16 | F = TypeVar("F", bound=Callable[..., Any]) 17 | 18 | 19 | def add_new_chat_to_db[F: Callable[..., Any]](func: F) -> F: 20 | @wraps(func) 21 | def wrapper(update: Update, context: CallbackContext, *args: Any, **kwargs: Any) -> F: 22 | assert update.effective_chat is not None 23 | assert update.effective_chat.id is not None 24 | assert (update.effective_chat.full_name or update.effective_chat.title) is not None 25 | add_chat_to_db( 26 | update.effective_chat.id, 27 | update.effective_chat.full_name or update.effective_chat.title, 28 | get_chat_type(update), 29 | ) 30 | return cast(F, func(update, context, *args, **kwargs)) 31 | 32 | return cast(F, wrapper) 33 | 34 | 35 | def analysis[F: Callable[..., Any]](func: F) -> F: 36 | @wraps(func) 37 | async def wrapper(update: Update, context: CallbackContext) -> None: 38 | lecture_info = await func(update, context) 39 | assert update.effective_message is not None 40 | if not lecture_info: 41 | return 42 | increment_usage(update.effective_message.chat_id) 43 | increment_series_requests(lecture_info) 44 | increment_lecture_requests(lecture_info) 45 | 46 | return cast(F, wrapper) 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # https://pre-commit.com 2 | default_install_hook_types: [ commit-msg, pre-commit ] 3 | default_stages: [ pre-commit, manual ] 4 | fail_fast: true 5 | repos: 6 | - repo: meta 7 | hooks: 8 | - id: check-useless-excludes 9 | - repo: https://github.com/pre-commit/pygrep-hooks 10 | rev: v1.10.0 11 | hooks: 12 | - id: python-check-mock-methods 13 | - id: python-use-type-annotations 14 | - id: rst-backticks 15 | - id: rst-directive-colons 16 | - id: rst-inline-touching-normal 17 | - id: text-unicode-replacement-char 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v6.0.0 20 | hooks: 21 | - id: check-added-large-files 22 | - id: check-ast 23 | - id: check-builtin-literals 24 | - id: check-case-conflict 25 | - id: check-docstring-first 26 | - id: check-json 27 | - id: check-merge-conflict 28 | - id: check-shebang-scripts-are-executable 29 | - id: check-symlinks 30 | - id: check-toml 31 | - id: check-vcs-permalinks 32 | - id: check-xml 33 | - id: check-yaml 34 | - id: debug-statements 35 | - id: destroyed-symlinks 36 | - id: detect-private-key 37 | - id: end-of-file-fixer 38 | types: [ python ] 39 | - id: fix-byte-order-marker 40 | - id: mixed-line-ending 41 | - id: name-tests-test 42 | args: [ --pytest-test-first ] 43 | - id: trailing-whitespace 44 | types: [ python ] 45 | - repo: https://github.com/astral-sh/ruff-pre-commit 46 | # Ruff version. 47 | rev: 'v0.14.9' 48 | hooks: 49 | - id: ruff 50 | args: [ --fix ] 51 | - id: ruff-format 52 | - repo: https://github.com/pre-commit/mirrors-mypy 53 | rev: 'v1.19.1' # Use the sha / tag you want to point at 54 | hooks: 55 | - id: mypy 56 | args: 57 | - --install-types 58 | additional_dependencies: 59 | - SQLAlchemy==2.0.45 60 | 61 | -------------------------------------------------------------------------------- /droos_bot/utils/telegram.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.abc import Callable 3 | from functools import wraps 4 | from pathlib import Path 5 | from time import sleep 6 | from typing import Any, TypeVar, cast 7 | 8 | from telegram import Update 9 | from telegram.error import BadRequest, Forbidden, RetryAfter 10 | from telegram.ext import Application 11 | 12 | F = TypeVar("F", bound=Callable[..., Any]) 13 | 14 | 15 | def tg_exceptions_handler[F: Callable[..., Any]](func: F) -> F: 16 | @wraps(func) 17 | def wrapper(*args: Any, **kwargs: Any) -> F: # type: ignore[return] 18 | try: 19 | return cast(F, func(*args, **kwargs)) 20 | except BadRequest as err: 21 | if "Message is not modified" in err.message: 22 | pass 23 | else: 24 | raise err 25 | 26 | except Forbidden as err: 27 | if "bot was blocked by the user" in err.message: 28 | pass 29 | else: 30 | raise err 31 | except RetryAfter as error: 32 | sleep(error.retry_after) 33 | return tg_exceptions_handler(cast(F, func(*args, **kwargs))) 34 | 35 | return cast(F, wrapper) 36 | 37 | 38 | def get_chat_type(update: Update) -> int: 39 | chat = update.effective_chat 40 | assert chat is not None 41 | return ( 42 | 0 43 | if chat.type == chat.PRIVATE 44 | else 1 45 | if chat.type == chat.GROUP 46 | else 2 # chat.type == chat.CHANNEL 47 | ) 48 | 49 | 50 | async def handle_restart(parent_dir: Path, application: Application) -> None: 51 | # Restart handler 52 | restart_message_path: Path = Path(f"{parent_dir.absolute()}/restart.json") 53 | if restart_message_path.exists(): 54 | restart_message = json.loads(restart_message_path.read_text()) 55 | await application.bot.edit_message_text( 56 | "Restarted Successfully!", 57 | restart_message["chat"], 58 | restart_message["message"], 59 | ) 60 | restart_message_path.unlink() 61 | -------------------------------------------------------------------------------- /droos_bot/modules/broadcast.py: -------------------------------------------------------------------------------- 1 | """broadcast module.""" 2 | 3 | import logging 4 | from asyncio import sleep 5 | 6 | from telegram import Message, Update 7 | from telegram.error import TelegramError 8 | from telegram.ext import CommandHandler, ContextTypes, filters 9 | 10 | from droos_bot import application 11 | from droos_bot.db.curd import get_all_chats 12 | from droos_bot.utils.filters import FilterBotAdmin 13 | from droos_bot.utils.telegram import tg_exceptions_handler 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | @tg_exceptions_handler 19 | async def broadcast(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 20 | """Broadcasts message to bot users.""" 21 | assert update.effective_message is not None 22 | assert update.effective_chat is not None 23 | message_to_send: Message = update.effective_message.reply_to_message 24 | failed_to_send = 0 25 | sent_successfully = 0 26 | for chat in get_all_chats(): 27 | try: 28 | await context.bot.copy_message( 29 | chat_id=chat.user_id, 30 | from_chat_id=message_to_send.chat.id, 31 | message_id=message_to_send.message_id, 32 | ) 33 | sent_successfully += 1 34 | await sleep(0.5) 35 | except TelegramError as err: 36 | failed_to_send += 1 37 | logger.warning(f"فشل إرسال الرسالة إلى {chat}:\n{err}") 38 | broadcast_status_message: str = ( 39 | f"اكتمل النشر! أرسلت الرسالة إلى {sent_successfully} مستخدم ومجموعة\n" 40 | ) 41 | if failed_to_send: 42 | broadcast_status_message += ( 43 | f" فشل الإرسال إلى {failed_to_send}مستخدمين/مجموعات، غالبا بسبب أن البوت طرد أو أوقف." 44 | ) 45 | await update.effective_message.reply_text( 46 | broadcast_status_message, 47 | reply_to_message_id=update.effective_message.message_id, 48 | ) 49 | 50 | 51 | application.add_handler( 52 | CommandHandler( 53 | "broadcast", broadcast, filters=filters.ChatType.PRIVATE & filters.REPLY & FilterBotAdmin() 54 | ) 55 | ) 56 | -------------------------------------------------------------------------------- /droos_bot/modules/stats.py: -------------------------------------------------------------------------------- 1 | """Bot stats module.""" 2 | 3 | from pandas import DataFrame 4 | from telegram import Update 5 | from telegram.ext import CommandHandler, ContextTypes, filters 6 | 7 | from droos_bot import application, sheet 8 | from droos_bot.db import Lecture, Series 9 | from droos_bot.db.curd import ( 10 | get_chats_count, 11 | get_top_lectures, 12 | get_top_series, 13 | get_usage_count, 14 | ) 15 | from droos_bot.utils.filters import FilterBotAdmin 16 | 17 | 18 | async def stats(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: 19 | stats_message = await update.message.reply_text("جاري تحضير الإحصائيات…") 20 | all_chats, active_chats = get_chats_count() 21 | usage_times, series_requests, lecture_requests = get_usage_count() 22 | top_series: list[Series] = get_top_series() 23 | top_lectures: list[Lecture] = get_top_lectures() 24 | 25 | message = ( 26 | f"المستخدمون الحاليون: {active_chats!s}\n" 27 | f"كل المستخدمون: {all_chats!s}\n" 28 | f"إجمالي مرات الاستخدام: {usage_times!s}\n" 29 | f"إجمالي الملفات المرسلة: {series_requests!s}\n" 30 | f"إجمالي الدروس المطلوبة: {lecture_requests!s}\n\n" 31 | f"أكثر السلاسل طلبًا:\n" 32 | f"{{top_series_placeholder}}\n" 33 | f"أكثر الدروس طلبًا:\n" 34 | f"{{top_lectures_placeholder}}\n" 35 | ) 36 | top_series_message = "" 37 | if top_series: 38 | for series in top_series: 39 | try: 40 | top_series_message += f" • {sheet.df[sheet.df.series_slug == series.id].iloc[0].series}: {series.requests!s} مرة\n" 41 | except IndexError: 42 | continue 43 | top_lectures_message = "" 44 | if top_lectures: 45 | for lecture in top_lectures: 46 | try: 47 | lecture_info: DataFrame = sheet.df[sheet.df.id == lecture.id] 48 | top_lectures_message += f" • {lecture_info.series.item()} ({lecture_info.lecture.item()}): {lecture.requests!s} مرة\n" 49 | except ValueError: 50 | continue 51 | message = message.format( 52 | top_series_placeholder=top_series_message, top_lectures_placeholder=top_lectures_message 53 | ) 54 | 55 | await stats_message.edit_text(message) 56 | 57 | 58 | application.add_handler(CommandHandler("stats", stats, filters.ChatType.PRIVATE & FilterBotAdmin())) 59 | -------------------------------------------------------------------------------- /droos_bot/__init__.py: -------------------------------------------------------------------------------- 1 | """Bot initialization.""" 2 | 3 | import json 4 | import logging.config 5 | from functools import partial 6 | from pathlib import Path 7 | 8 | from telegram.constants import ParseMode 9 | from telegram.ext import ApplicationBuilder, Defaults, PicklePersistence 10 | 11 | from droos_bot.gsheet.spreadsheet import Spreadsheet 12 | from droos_bot.utils.telegram import handle_restart 13 | 14 | # paths 15 | WORK_DIR = Path(__package__) 16 | PARENT_DIR = WORK_DIR.parent 17 | 18 | # bot config 19 | CONFIG = json.loads((PARENT_DIR / "config.json").read_text(encoding="utf-8")) 20 | BOT_TOKEN = CONFIG["tg_bot_token"] 21 | TG_BOT_ADMINS = CONFIG["tg_bot_admins"] 22 | DATA_COLUMNS: dict[str, str] = CONFIG["data_columns"] 23 | LECTURE_COMPONENTS: dict[str, str] = CONFIG.get( 24 | "lecture_components", 25 | { 26 | "book": "📕 الكتاب", 27 | "main": "📝 المحاور", 28 | "video": "🎞 فيديو", 29 | "voice": "🎧 صوتي", 30 | "text": "📄 تفريغ", 31 | "summary": "📎 ملخص", 32 | }, 33 | ) 34 | 35 | # Logging 36 | log_file_path = PARENT_DIR / "last_run.log" 37 | logging_config = { 38 | "version": 1, 39 | "disable_existing_loggers": False, 40 | "formatters": { 41 | "detailed": { 42 | "format": "%(asctime)s [%(levelname)s] %(name)s [%(module)s.%(funcName)s:%(lineno)d]: %(message)s", 43 | "datefmt": "%Y-%m-%d %H:%M:%S", 44 | }, 45 | }, 46 | "handlers": { 47 | "file": { 48 | "class": "logging.handlers.TimedRotatingFileHandler", 49 | "level": "INFO", 50 | "formatter": "detailed", 51 | "filename": log_file_path, 52 | "when": "midnight", 53 | "interval": 1, 54 | "backupCount": 7, 55 | }, 56 | "console": { 57 | "class": "logging.StreamHandler", 58 | "level": "INFO", 59 | "formatter": "detailed", 60 | "stream": "ext://sys.stdout", 61 | }, 62 | }, 63 | "loggers": { 64 | "": { # root logger 65 | "handlers": ["file", "console"], 66 | "level": "INFO", 67 | "propagate": True, 68 | }, 69 | }, 70 | } 71 | logging.config.dictConfig(logging_config) 72 | 73 | # bot 74 | persistence = PicklePersistence(filepath=f"{PARENT_DIR}/bot.pickle") 75 | defaults = Defaults(parse_mode=ParseMode.HTML, disable_web_page_preview=True) 76 | 77 | application = ( 78 | ApplicationBuilder() 79 | .token(BOT_TOKEN) 80 | .defaults(defaults) 81 | .persistence(persistence) 82 | .post_init(partial(handle_restart, PARENT_DIR)) 83 | .build() 84 | ) 85 | sheet = Spreadsheet( 86 | f"{PARENT_DIR}/service_account.json", 87 | CONFIG["sheet_id"], 88 | CONFIG["sheet_name"], 89 | DATA_COLUMNS, 90 | LECTURE_COMPONENTS, 91 | ) 92 | 93 | logging.getLogger("httpx").setLevel(logging.WARNING) 94 | -------------------------------------------------------------------------------- /droos_bot/gsheet/spreadsheet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | from gspread_pandas import Client, Spread, conf 6 | from pandas import DataFrame 7 | from pyarabic.trans import convert as transliterate 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Spreadsheet: 13 | def __init__( 14 | self, 15 | service_account: str, 16 | sheet_id: str, 17 | sheet_name: str, 18 | data_columns: dict[str, str], 19 | lecture_components: dict[str, str], 20 | ) -> None: 21 | self._client: Client = Client( 22 | config=conf.get_config( 23 | conf_dir=str(Path(service_account).parent), file_name=service_account 24 | ) 25 | ) 26 | self.worksheet: Spread = Spread(sheet_id, sheet=sheet_name, client=self._client) 27 | self.df: DataFrame = self.worksheet.sheet_to_df().iloc[1:] 28 | # Add slug columns 29 | for column_id in data_columns: 30 | self.df[f"{column_id}_slug"] = self.df[column_id].apply( 31 | lambda x: "_".join( 32 | transliterate(str(x), "arabic", "tim").lower().replace("\\", "").split(" ") 33 | ) 34 | ) 35 | # Add id column 36 | self.df["id"] = self.df.apply( 37 | lambda row: "_".join( 38 | [row[f"{col_id}_slug"] for col_id in data_columns] + [row["lecture"]] 39 | ), 40 | axis=1, 41 | ) 42 | self.hierarchy = self.create_hierarchy(data_columns, lecture_components) 43 | 44 | def create_hierarchy( 45 | self, data_columns: dict[str, str], lecture_components: dict[str, str] 46 | ) -> dict[str, Any]: 47 | hierarchy: dict[str, Any] = {} 48 | for data_column_id, data_column_name in data_columns.items(): 49 | hierarchy[data_column_name] = {} 50 | name: tuple[str, ...] 51 | data: DataFrame 52 | for name, data in self.df.groupby( 53 | [data_column_id] if data_column_id == "series" else [data_column_id, "series"], 54 | sort=False, 55 | dropna=True, 56 | ): 57 | current_level = hierarchy[data_column_name] 58 | for level in name: 59 | if level not in current_level: 60 | current_level[level.strip()] = {} 61 | current_level = current_level[level.strip()] 62 | for index, row in enumerate(data.itertuples(), start=1): 63 | current_level[str(row.lecture) or str(index)] = { 64 | component: getattr(row, component) for component in lecture_components 65 | } | {"__data": True, "id": row.id} 66 | return hierarchy 67 | 68 | def navigate_hierarchy(self, path: list[str]) -> dict | None: 69 | current_level = self.hierarchy 70 | for item in path: 71 | if item in current_level: 72 | current_level = current_level[item] 73 | else: 74 | return None 75 | return current_level 76 | -------------------------------------------------------------------------------- /droos_bot/db/curd.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pandas import DataFrame 4 | from sqlalchemy import Row 5 | from sqlalchemy.exc import MultipleResultsFound 6 | from sqlalchemy.sql.functions import sum as sql_sum 7 | 8 | from droos_bot.db import Lecture 9 | from droos_bot.db.models.chat import Chat 10 | from droos_bot.db.models.series import Series 11 | from droos_bot.db.session import session 12 | 13 | 14 | def increment_series_requests(series_info: dict[str, Any]) -> None: 15 | series_id = "_".join(series_info["id"].split("_")[:-1]) 16 | series: Series | None = session.query(Series).filter(Series.id == series_id).first() 17 | if series: 18 | series.requests += 1 19 | else: 20 | session.add(Series(id=series_id, requests=1)) 21 | session.commit() 22 | 23 | 24 | def increment_lecture_requests(lecture_info: DataFrame) -> None: 25 | lecture: Lecture | None = ( 26 | session.query(Lecture).filter(Lecture.id == lecture_info["id"]).first() 27 | ) 28 | if lecture: 29 | lecture.requests += 1 30 | else: 31 | session.add(Lecture(id=lecture_info["id"], requests=1)) 32 | session.commit() 33 | 34 | 35 | def add_chat_to_db(user_id: int, user_name: str, chat_type: int) -> None: 36 | user: Chat | None = session.query(Chat).filter(Chat.user_id == user_id).first() 37 | if not user: 38 | session.add(Chat(user_id=user_id, user_name=user_name, type=chat_type)) 39 | elif user.user_name != user_name: 40 | user.user_name = user_name 41 | session.commit() 42 | 43 | 44 | def increment_usage(user_id: int) -> None: 45 | chat = session.query(Chat).filter(Chat.user_id == user_id).first() 46 | if not chat: 47 | return 48 | chat.usage_times += 1 49 | session.commit() 50 | 51 | 52 | def get_chats_count() -> tuple[int, int]: 53 | all_chats = session.query(Chat).count() 54 | active_chats = session.query(Chat).filter(Chat.usage_times > 0).count() 55 | return all_chats, active_chats 56 | 57 | 58 | def get_usage_count() -> tuple[int, int, int]: 59 | usage_times_row: Row | None = session.query( 60 | sql_sum(Chat.usage_times).label("usage_times") 61 | ).first() 62 | usage_times: int = usage_times_row.usage_times if usage_times_row else 0 63 | series_requests_row: Row | None = session.query( 64 | sql_sum(Series.requests).label("series_requests") 65 | ).first() 66 | series_requests: int = series_requests_row.series_requests if series_requests_row else 0 67 | lecture_requests_row: Row | None = session.query( 68 | sql_sum(Lecture.requests).label("lecture_requests") 69 | ).first() 70 | lecture_requests: int = lecture_requests_row.lecture_requests if lecture_requests_row else 0 71 | return usage_times, series_requests, lecture_requests 72 | 73 | 74 | def get_top_series() -> list[Series]: 75 | return session.query(Series).order_by(Series.requests.desc()).limit(5).all() 76 | 77 | 78 | def get_top_lectures() -> list[Lecture]: 79 | return session.query(Lecture).order_by(Lecture.requests.desc()).limit(5).all() 80 | 81 | 82 | def get_all_chats() -> list[Chat]: 83 | return session.query(Chat).all() 84 | 85 | 86 | def get_chat_id_by_name(name: str) -> int: 87 | try: 88 | return session.query(Chat.user_id).filter(Chat.user_name.like(f"%{name}%")).scalar() or 0 89 | except MultipleResultsFound: 90 | return 0 91 | -------------------------------------------------------------------------------- /droos_bot/utils/keyboards.py: -------------------------------------------------------------------------------- 1 | from telegram import ReplyKeyboardMarkup 2 | from telegram.constants import InlineKeyboardMarkupLimit 3 | 4 | from droos_bot import CONFIG, DATA_COLUMNS 5 | 6 | 7 | def create_keyboard( # noqa: PLR0913 8 | items: list[str], 9 | show_back: bool = True, 10 | show_pagination: bool = True, 11 | show_bullet: bool = True, 12 | current_page: int = 0, 13 | items_per_page: int = 10, 14 | max_pages_displayed: int = InlineKeyboardMarkupLimit.BUTTONS_PER_ROW - 3, 15 | ) -> ReplyKeyboardMarkup: 16 | """Generate pagination numbers with visual indicators. 17 | 18 | :param items: list of keyboard buttons 19 | :param show_back: Add back button to the keyboard 20 | :param show_pagination: Add pagination buttons 21 | :param show_bullet: Add bullet before each button 22 | :param current_page: The current page number (0-based). 23 | :param items_per_page: The number of items to display per page. 24 | :param max_pages_displayed: Max pages displayed (excl. first & last). Defaults to 5. 25 | :return: Pagination strings with indicators. 26 | 27 | Indicators: 28 | - 🔶 Current page 29 | - « Before home (only if not on the first page) 30 | - » After end (only if not on the last page) 31 | - ← Before current page 32 | - → After current page 33 | """ 34 | total_pages = (len(items) + items_per_page - 1) // items_per_page 35 | current_page = current_page if current_page >= 0 else total_pages - 1 36 | # Calculate start and end pages for display 37 | half_display = max_pages_displayed // 2 38 | start_page = max(1, current_page + 1 - half_display) 39 | end_page = min(total_pages, start_page + max_pages_displayed) 40 | # Adjust start page if necessary to show max_pages_displayed pages 41 | if end_page - start_page < max_pages_displayed and start_page > 1: 42 | start_page = end_page - max_pages_displayed + 1 43 | 44 | pagination = [] 45 | if show_pagination and total_pages > 1: 46 | # Add "«" if not on the first page 47 | if current_page > 0: 48 | pagination.append("«") 49 | 50 | for page in range(start_page, end_page + 1): 51 | if page == current_page + 1: 52 | pagination.append(f"🔶{page}") 53 | elif page < current_page + 1: 54 | pagination.append(f"←{page}") 55 | else: 56 | pagination.append(f"{page}→") 57 | 58 | # Add "»" if not on the last page 59 | if current_page < total_pages - 1: 60 | pagination.append("»") 61 | 62 | # slice data 63 | start_idx = current_page * items_per_page 64 | end_idx = start_idx + items_per_page 65 | keyboard = [[f"• {item}" if show_bullet else item] for item in items[start_idx:end_idx]] 66 | 67 | # finalize the keyboard 68 | bottom_row = ["القائمة الرئيسية 🏠"] 69 | if show_back: 70 | bottom_row.append("🔙 رجوع") 71 | if pagination: 72 | keyboard.append(pagination) 73 | keyboard.append(bottom_row) 74 | 75 | return ReplyKeyboardMarkup(keyboard, resize_keyboard=True) 76 | 77 | 78 | def create_main_keyboard() -> list[list[str]]: 79 | data_values = [ 80 | [value] for key, value in DATA_COLUMNS.items() if key not in CONFIG.get("hide", []) 81 | ] 82 | main_keyboard_buttons = [*data_values] 83 | buttons_to_disable = CONFIG.get("disable", []) 84 | for item in ["إرسال مواد", "التواصل والاقتراحات", "البحث في المحتوى"]: 85 | if item not in buttons_to_disable: 86 | main_keyboard_buttons.append([item]) 87 | return main_keyboard_buttons 88 | 89 | 90 | main_keyboard = ReplyKeyboardMarkup(create_main_keyboard()) 91 | cancel_search_keyboard = ReplyKeyboardMarkup([["القائمة الرئيسية 🏠"], ["إلغاء البحث"]]) 92 | cancel_keyboard = ReplyKeyboardMarkup([["إنهاء"]]) 93 | -------------------------------------------------------------------------------- /droos_bot/modules/search.py: -------------------------------------------------------------------------------- 1 | """Data search module.""" 2 | 3 | from typing import cast 4 | 5 | from telegram import Update 6 | from telegram.ext import ( 7 | CommandHandler, 8 | ContextTypes, 9 | ConversationHandler, 10 | MessageHandler, 11 | filters, 12 | ) 13 | 14 | from droos_bot import DATA_COLUMNS, application, sheet 15 | from droos_bot.utils.keyboards import cancel_search_keyboard, create_keyboard, main_keyboard 16 | from droos_bot.utils.telegram import tg_exceptions_handler 17 | 18 | START_SEARCH = 0 19 | 20 | 21 | @tg_exceptions_handler 22 | async def search_handler(update: Update, _: ContextTypes.DEFAULT_TYPE) -> int: 23 | assert update.effective_message is not None 24 | await update.message.reply_text( 25 | "اكتب ما تريد البحث عنه", 26 | reply_to_message_id=update.effective_message.message_id, 27 | reply_markup=cancel_search_keyboard, 28 | ) 29 | return START_SEARCH 30 | 31 | 32 | @tg_exceptions_handler 33 | async def search_for_text(update: Update, _: ContextTypes.DEFAULT_TYPE) -> int | None: 34 | assert update.effective_message is not None 35 | search_text = update.effective_message.text.strip() 36 | match = sheet.df[ 37 | sheet.df[list(DATA_COLUMNS.keys())] 38 | .astype(str) 39 | .apply(lambda x: x.str.contains(search_text, case=False, regex=True, na=False)) 40 | .any(axis=1) 41 | ] 42 | if match.empty: 43 | await update.message.reply_text( 44 | "لا يوجد نتائج", reply_to_message_id=update.effective_message.message_id 45 | ) 46 | return None 47 | melted = match.melt( 48 | id_vars=["id"], value_vars=list(DATA_COLUMNS.keys()), var_name="column", value_name="value" 49 | ) 50 | melted = melted[melted["value"].notna()] 51 | grouped_results = { 52 | DATA_COLUMNS[k]: v.tolist() 53 | for k, v in melted.groupby("column")["value"].unique().to_dict().items() 54 | } 55 | reply_markup = create_keyboard( 56 | [ 57 | f"{key} > {value}" 58 | for key, values in reversed(grouped_results.items()) 59 | for value in values 60 | ], 61 | show_back=False, 62 | show_pagination=False, 63 | ) 64 | await update.message.reply_text( 65 | "نتائج البحث:", 66 | reply_markup=reply_markup, 67 | reply_to_message_id=update.effective_message.message_id, 68 | ) 69 | return cast(int, ConversationHandler.END) 70 | 71 | 72 | async def cancel_search_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: 73 | context.user_data["path"] = [] 74 | await update.message.reply_text( 75 | "يمكنك متابعة استخدام البوت من خلال الأزرار الظاهرة بالأسفل", 76 | reply_markup=main_keyboard, 77 | ) 78 | return cast(int, ConversationHandler.END) 79 | 80 | 81 | application.add_handler( 82 | ConversationHandler( 83 | entry_points=[MessageHandler(filters.Regex("^البحث في المحتوى$"), search_handler)], 84 | states={ 85 | START_SEARCH: [ 86 | MessageHandler( 87 | filters.ChatType.PRIVATE 88 | & filters.TEXT 89 | & ~( 90 | filters.COMMAND 91 | | filters.Regex("^البحث في المحتوى$") 92 | | filters.Regex("^إلغاء البحث$") 93 | ), 94 | search_for_text, 95 | ) 96 | ], 97 | }, 98 | fallbacks=[ 99 | CommandHandler("cancel", cancel_search_handler, filters.ChatType.PRIVATE), 100 | MessageHandler( 101 | filters.ChatType.PRIVATE & filters.Regex("^إلغاء البحث$"), cancel_search_handler 102 | ), 103 | MessageHandler( 104 | filters.ChatType.PRIVATE & filters.Regex("^(القائمة الرئيسية 🏠)$"), 105 | cancel_search_handler, 106 | ), 107 | ], 108 | ) 109 | ) 110 | -------------------------------------------------------------------------------- /droos_bot/modules/droos.py: -------------------------------------------------------------------------------- 1 | """Droos handler module.""" 2 | 3 | import re 4 | from typing import Any 5 | 6 | from telegram import Update 7 | from telegram.ext import ContextTypes, MessageHandler, filters 8 | 9 | from droos_bot import DATA_COLUMNS, LECTURE_COMPONENTS, application, sheet 10 | from droos_bot.modules.start import start_handler 11 | from droos_bot.utils.analytics import add_new_chat_to_db, analysis 12 | from droos_bot.utils.keyboards import create_keyboard 13 | from droos_bot.utils.telegram import tg_exceptions_handler 14 | 15 | COMPONENTS_KEYS = {v: k for k, v in LECTURE_COMPONENTS.items()} 16 | 17 | 18 | async def parse_telegram_link(telegram_link: str) -> tuple[str, str]: 19 | telegram_chat, message_id = telegram_link.split("/")[-2:] 20 | if telegram_chat.startswith("-") or telegram_chat.isdigit(): 21 | telegram_chat_id = telegram_chat 22 | elif application.bot_data.get("chats", {}).get(telegram_chat): 23 | telegram_chat_id = application.bot_data["chats"][telegram_chat] 24 | else: 25 | telegram_chat_id = (await application.bot.get_chat(f"@{telegram_chat}")).id 26 | if application.bot_data.get("chats"): 27 | application.bot_data["chats"][telegram_chat] = telegram_chat_id 28 | else: 29 | application.bot_data["chats"] = {telegram_chat: telegram_chat_id} 30 | return telegram_chat_id, message_id 31 | 32 | 33 | @tg_exceptions_handler 34 | @add_new_chat_to_db 35 | async def handle_navigation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 36 | query = update.message.text.lstrip("•").strip() 37 | user_data = context.user_data 38 | page = 0 39 | if query == "🔙 رجوع": 40 | if user_data.get("path"): 41 | user_data["path"].pop() 42 | if not user_data.get("path"): 43 | return await start_handler(update, context) 44 | elif any(i in query for i in ("«", "»")): 45 | # Handle first and last page navigation 46 | page = 0 if "«" in query else -1 47 | elif ( 48 | query.startswith("🔶") 49 | or any(i in query for i in ("←", "→")) 50 | or (query.isdigit() and not update.message.text.startswith("•")) 51 | ): 52 | match = re.search(r"(\d+)", query) 53 | page = int(match.group(1)) - 1 if match else 0 54 | else: 55 | if "path" not in user_data: 56 | user_data["path"] = [] 57 | # search results 58 | if " > " in query: 59 | user_data["path"] = query.split(" > ") 60 | else: 61 | user_data["path"].append(query) 62 | 63 | current_level = sheet.navigate_hierarchy(user_data["path"]) 64 | if current_level is None: 65 | return await start_handler(update, context) 66 | 67 | if "__data" in current_level: 68 | await update.message.reply_text( 69 | ( 70 | f"السلسلة: 🗂{user_data['path'][-2]}\n📚 الدرس: {user_data['path'][-1]}\n" 71 | ), 72 | reply_markup=create_keyboard( 73 | [value for key, value in LECTURE_COMPONENTS.items() if current_level.get(key)], 74 | show_back=True, 75 | show_pagination=False, 76 | show_bullet=False, 77 | ), 78 | ) 79 | else: 80 | await update.message.reply_text( 81 | f"{' > '.join(user_data.get('path', [])) or 'اختر ما تريد'}", 82 | reply_markup=create_keyboard( 83 | list(current_level.keys()), 84 | show_back=bool(user_data.get("path", [])), 85 | current_page=page, 86 | ), 87 | ) 88 | return None 89 | 90 | 91 | @tg_exceptions_handler 92 | @analysis 93 | async def handle_resource_selection( 94 | update: Update, context: ContextTypes.DEFAULT_TYPE 95 | ) -> dict[str, Any] | None: 96 | query = update.message.text.lower() 97 | current_path = context.user_data.get("path", []) 98 | current_level = sheet.navigate_hierarchy(current_path) 99 | if not current_level or not current_level.get("__data"): 100 | await update.message.reply_text("لم يتم العثور على الدرس المطلوب") 101 | await start_handler(update, context) 102 | return None 103 | from_chat_id, message_id = await parse_telegram_link(current_level[COMPONENTS_KEYS[query]]) 104 | await application.bot.copy_message( 105 | chat_id=update.message.chat_id, 106 | from_chat_id=from_chat_id, 107 | message_id=message_id, 108 | ) 109 | return current_level 110 | 111 | 112 | # lecture components 113 | application.add_handler( 114 | MessageHandler( 115 | filters.Regex(f"^({'|'.join(i for i in LECTURE_COMPONENTS.values())})$"), 116 | handle_resource_selection, 117 | ) 118 | ) 119 | # data columns 120 | for _data_column_id, _data_column_name in DATA_COLUMNS.items(): 121 | application.add_handler( 122 | MessageHandler( 123 | filters.ChatType.PRIVATE & filters.Regex(_data_column_name), 124 | handle_navigation, 125 | ) 126 | ) 127 | # navigation 128 | application.add_handler( 129 | MessageHandler( 130 | filters.ChatType.PRIVATE & ~filters.COMMAND & (filters.Regex(r"[«»←→🔶\d]|🔙 رجوع|•")), 131 | handle_navigation, 132 | ) 133 | ) 134 | -------------------------------------------------------------------------------- /droos_bot/modules/feedback_and_files.py: -------------------------------------------------------------------------------- 1 | """Feedback and files handler module.""" 2 | 3 | from telegram import MessageOriginHiddenUser, MessageOriginUser, Update 4 | from telegram.constants import MessageLimit 5 | from telegram.ext import ( 6 | CommandHandler, 7 | ContextTypes, 8 | ConversationHandler, 9 | MessageHandler, 10 | filters, 11 | ) 12 | 13 | from droos_bot import CONFIG, application 14 | from droos_bot.db.curd import get_chat_id_by_name 15 | from droos_bot.modules.search import cancel_search_handler 16 | from droos_bot.utils.filters import FeedbackMessageFilter, FilterBotAdmin 17 | from droos_bot.utils.keyboards import cancel_keyboard 18 | from droos_bot.utils.telegram import tg_exceptions_handler 19 | 20 | START_RECEIVING_FEEDBACK = 1 21 | START_RECEIVING_FILES = 2 22 | notice_message = ( 23 | "بعد الانتهاء اضغط على زر إنهاء الموجود بالأسفل\n" 24 | "إذا كنت ترغب في أن نتواصل معك رجاء اترك معرف أو رابط حساب ليتواصل معك المشرفون من خلاله إن شاء الله " 25 | "إذا كان معرف حسابك لا يظهر عند إعادة توجيه الرسائل" 26 | ) 27 | 28 | 29 | @tg_exceptions_handler 30 | async def feedback_handler(update: Update, _: ContextTypes.DEFAULT_TYPE) -> int: 31 | """Handle feedback from users.""" 32 | assert update.effective_message is not None 33 | await update.message.reply_text( 34 | "يمكنك كتابة ما تريد إرساله للمشرفين على البوت هنا\n" + notice_message, 35 | reply_to_message_id=update.effective_message.message_id, 36 | reply_markup=cancel_keyboard, 37 | ) 38 | return START_RECEIVING_FEEDBACK 39 | 40 | 41 | @tg_exceptions_handler 42 | async def forward_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 43 | assert update.effective_message is not None 44 | assert update.effective_chat is not None 45 | await context.bot.forward_message( 46 | chat_id=CONFIG["tg_feedback_chat_id"], 47 | from_chat_id=update.effective_chat.id, 48 | message_id=update.effective_message.message_id, 49 | ) 50 | await update.message.reply_text( 51 | r"تم استلام رسالتك بنجاح\. بإمكانك إرسال المزيد أو الضغط على زر إنهاء للعودة" 52 | ) 53 | 54 | 55 | @tg_exceptions_handler 56 | async def files_handler(update: Update, _: ContextTypes.DEFAULT_TYPE) -> int: 57 | """Handle files from users.""" 58 | assert update.effective_message is not None 59 | await update.message.reply_text( 60 | "يمكنك إرسال مواد لإضافتها للبوت هنا\n" + notice_message, 61 | reply_to_message_id=update.effective_message.message_id, 62 | reply_markup=cancel_keyboard, 63 | ) 64 | return START_RECEIVING_FILES 65 | 66 | 67 | @tg_exceptions_handler 68 | async def reply_to_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 69 | assert update.effective_message is not None 70 | assert update.effective_chat is not None 71 | reply_to_message = update.effective_message.reply_to_message 72 | if reply_to_message.forward_origin and isinstance( 73 | reply_to_message.forward_origin, MessageOriginHiddenUser 74 | ): 75 | chat_id = get_chat_id_by_name(reply_to_message.forward_origin.sender_user_name) 76 | if not chat_id: 77 | await update.effective_message.reply_text( 78 | "لا يمكن الرد على هذا المستخدم بسبب إعدادات حسابه", 79 | reply_to_message_id=update.effective_message.message_id, 80 | ) 81 | return 82 | elif not reply_to_message.forward_origin and reply_to_message.from_user.is_bot: 83 | return 84 | else: 85 | assert isinstance(reply_to_message.forward_origin, MessageOriginUser) 86 | chat_id = reply_to_message.forward_origin.sender_user.id 87 | replied_to_message_text = reply_to_message.text_html_urled or "" 88 | reply_with_message_text = ( 89 | f"رد المشرف على رسالتك السابقة:\n\n{replied_to_message_text[: MessageLimit.MAX_TEXT_LENGTH - 150]}\n\n" 90 | f"ملاحظة:\nللرد على هذه الرسالة اضغط على زر التواصل والاقتراحات أولا ثم أرسل الرد" 91 | ) 92 | await context.bot.send_message( 93 | chat_id, 94 | reply_with_message_text, 95 | ) 96 | await context.bot.copy_message( 97 | chat_id=chat_id, 98 | from_chat_id=update.effective_chat.id, 99 | message_id=update.effective_message.message_id, 100 | ) 101 | admin_message = ( 102 | f'رُد على ' 103 | f'' 104 | f"{reply_to_message.forward_origin.sender_user_name if hasattr(reply_to_message.forward_origin, 'sender_user_name') else reply_to_message.forward_origin.sender_user.full_name} " 105 | f'بهذا الرد' 106 | ) 107 | await update.effective_message.reply_text( 108 | admin_message, 109 | reply_to_message_id=update.effective_message.message_id, 110 | ) 111 | 112 | 113 | feedback_conversation_handler = ConversationHandler( 114 | entry_points=[MessageHandler(filters.Regex("^التواصل والاقتراحات$"), feedback_handler)], 115 | states={ 116 | START_RECEIVING_FEEDBACK: [ 117 | MessageHandler( 118 | filters.TEXT 119 | & ~( 120 | filters.COMMAND 121 | | filters.Regex("^التواصل والاقتراحات$") 122 | | filters.Regex("^إنهاء$") 123 | ), 124 | forward_feedback, 125 | ) 126 | ], 127 | }, 128 | fallbacks=[ 129 | CommandHandler("cancel", cancel_search_handler), 130 | MessageHandler(filters.Regex("^إنهاء$"), cancel_search_handler), 131 | ], 132 | ) 133 | 134 | files_conversation_handler = ConversationHandler( 135 | entry_points=[MessageHandler(filters.Regex("^إرسال مواد$"), files_handler)], 136 | states={ 137 | START_RECEIVING_FILES: [ 138 | MessageHandler( 139 | filters.PHOTO | filters.VIDEO | filters.AUDIO | filters.Document.ALL, 140 | forward_feedback, 141 | ) 142 | ], 143 | }, 144 | fallbacks=[ 145 | CommandHandler("cancel", cancel_search_handler), 146 | MessageHandler(filters.Regex("^إنهاء$"), cancel_search_handler), 147 | ], 148 | ) 149 | 150 | if "التواصل والاقتراحات" not in CONFIG.get("disable", []): 151 | application.add_handler(feedback_conversation_handler) 152 | if "إرسال مواد" not in CONFIG.get("disable", []): 153 | application.add_handler(files_conversation_handler) 154 | 155 | filter_feedback_message = FeedbackMessageFilter( 156 | CONFIG["tg_feedback_chat_id"], CONFIG["tg_bot_token"].split(":")[0] 157 | ) 158 | application.add_handler( 159 | MessageHandler(FilterBotAdmin() & filter_feedback_message, reply_to_feedback) 160 | ) 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # بوت الدروس العلمية 2 | 3 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) 4 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 5 | 6 | [![PayPal](https://img.shields.io/badge/PayPal-Donate-00457C?style=flat&labelColor=00457C&logo=PayPal&logoColor=white&link=https://www.paypal.me/yshalsager)](https://www.paypal.me/yshalsager) 7 | [![Patreon](https://img.shields.io/badge/Patreon-Support-F96854?style=flat&labelColor=F96854&logo=Patreon&logoColor=white&link=https://www.patreon.com/XiaomiFirmwareUpdater)](https://www.patreon.com/XiaomiFirmwareUpdater) 8 | [![Liberapay](https://img.shields.io/badge/Liberapay-Support-F6C915?style=flat&labelColor=F6C915&logo=Liberapay&logoColor=white&link=https://liberapay.com/yshalsager)](https://liberapay.com/yshalsager) 9 | 10 | بوت تليجرام مكتوب بلغة بايثون لعرض المواد العلمية لبعض الشيوخ بطريقة منظمة وسهلة التعديل للمشرفين على البوت. 11 | 12 | ## ميزات البوت 13 | 14 | - عرض سلاسل الدروس العلمية مهما كان عددها بطريقة منظمة ومقسمة إلى صفحات تشبه نتائج محركات البحث. 15 | - عرض محتوى كل سلسلة بنفس الطريقة، مع أزرار لإرسال النسخ الصوتية والمرئية وتفريغات وتلخيصات الدروس. 16 | - البحث في محتوى البوت. 17 | - التواصل مع المشرفين. 18 | - إرسال مواد لمشرفي البوت. 19 | - الوصول السريع لوظائف البوت عن طريق لوحة مفاتيح أزرار. 20 | - الرد على المستخدمين بالرد على رسائلهم داخل مجموعة استلام الرسائل. 21 | - إحصائيات مستخدمي البوت وأكثر السلاسل والدروس طلبا (للمشرفين فقط). [`/stats`] 22 | - إرسال رسالة لكل المشتركين في البوت (للمشرفين فقط). [`/broadcast` في رد على رسالة] 23 | - إمكانية إعادة تشغيل البوت لتحميل البيانات مرة أخرى من التليجرام مباشرة (للمشرفين فقط). [`/restart`] 24 | - تحديث الكود الخاص بالبوت في حالة استخدام git. [`/update`] 25 | 26 | ## كيفية استخدام البوت 27 | 28 | - ابدأ البوت عن طريق الضغط على زر start أو البَدْء، ثم استخدم لوحة اﻷزرار لاستخدام وظائف البوت المختلفة مثل "السلاسل 29 | العلمية - البحث عن سلسلة - إرسال مواد - التواصل والاقتراحات". 30 | 31 | ## كيفية عمل البوت 32 | 33 | البوت يعتمد 34 | على [Google Sheet](https://docs.google.com/spreadsheets/d/1o2016c9JQDROnhAhveKq70pF07_FozozP2xF7ekijTM/edit?usp=sharing) 35 | يقوم المشرفين والمتطوعين بملئها بالشكل التالي: 36 | 37 | | series | lecture | book | main | video | voice | text | summary | 38 | |------------------|--------------|--------------------------------------------------|--------------------------------------------------|--------------------------------------------------|--------------------------------------------------|--------------------------------------------------|---------| 39 | | اسم السلسلة | رقم المحاضرة | كتاب | أهم المحاور | مرئي | صوتي | تفريغ | تلخيص | 40 | | سلسلة دروس تفسير | 1 | [https://t.me/channel/4](https://t.me/channel/4) | [https://t.me/channel/4](https://t.me/channel/4) | [https://t.me/channel/4](https://t.me/channel/4) | [https://t.me/channel/4](https://t.me/channel/4) | [https://t.me/channel/4](https://t.me/channel/4) | | 41 | 42 | - series: اسم السلسلة: اسم سلسلة الدروس 43 | - lecture: رقم المحاضرة: رقم الدرس داخل سلسلة الدروس 44 | - book: الكتاب: رابط تليجرام لملف PDF الكتاب الذي يشرح في السلسلة أو صوت أو فيديو أو مستند به أهم محاور الدرس 45 | - main: أهم المحاور: رابط تليجرام لصورة أو صوت أو فيديو أو مستند به أهم محاور الدرس 46 | - video: مرئي: رابط تليجرام لنسخة الفيديو من الدرس 47 | - voice: صوتي: رابط تليجرام للنسخة الصوتية من الدرس 48 | - text: تفريغ: رابط تليجرام لمستند تفريغ الدرس 49 | - summary: تلخيص: رابط تليجرام لمستند ملخص الدرس 50 | 51 | يحول البوت الرسائل للمستخدمين بالاعتماد على روابط المواد. 52 | 53 | ## إعداد البوت (للمطورين) 54 | 55 | ### إعداد حساب خدمة للتعامل مع Google Sheet 56 | 57 | - ادخل إلى منصة جوجل للمطورين وأنشئ مشروعا إن لم يكن لديك واحد، ثم فعل الوصول 58 | إلى [واجهة برمجة Google Sheet](https://console.developers.google.com/marketplace/product/google/sheets.googleapis.com). 59 | - ادخل إلى [صفحة إعداد الصلاحيات](https://console.developers.google.com/apis/api/sheets.googleapis.com/credentials)، 60 | وأضف حساب خدمة جديد بصلاحيات القراءة فقط "viewer". 61 | - أنشئ مفاتيح للحساب ثم حمل ملف ال JSON إلى مجلد المشروع باسم `service_account.json`. 62 | - أضف عناوين حسابات الخدمة إلى Google Sheet التي بها المواد لتتمكن الحسابات من الوصول إليها. 63 | 64 | ### إعداد ملف التهيئة 65 | 66 | - انسخ هذا المستودع. 67 | - انسخ ملف `config.example.json` باسم `config.json` واملأ المعلومات المطلوبة: 68 | 69 | | البيانات | معلومات | ملاحظات | 70 | |:-------------------:|:----------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| 71 | | tg_bot_token | التوكن الخاص بواجهة برمجة تليجرام للبوتات | إجباري | 72 | | tg_bot_admins | قائمة معرفات مشرفي البوت | إجباري | 73 | | tg_feedback_chat_id | معرف المحادثة التي سيرسل لها الملفات ورسائل التواصل مع المشرفين | إجباري | 74 | | sheet_id | معرف Google Sheet الخاصة بمواد البوت | إجباري | 75 | | sheet_name | اسم Google Sheet التي ستستخدم | إجباري
يمكن الاستعانة بهذا [النموذج](https://docs.google.com/spreadsheets/d/1o2016c9JQDROnhAhveKq70pF07_FozozP2xF7ekijTM/edit?usp=sharing) لمعرفة كيفية ملء بيانات المواد | 76 | | data_columns | الصفوف التي ستستخدم في عرض المحتوى والبحث فيه | اختياري (يستخدم الافتراضي عند غيابه) | 77 | | lecture_components | مكونات كل درس | اختياري (يستخدم الافتراضي عند غيابه) | 78 | | hide | إخفاء بعض الصفوف من القائمة الرئيسية | اختياري (يستخدم الافتراضي عند غيابه) | 79 | | disable | تعطيل خيارات إرسال مواد - التواصل والاقتراحات - البحث في المحتوى | اختياري (يستخدم الافتراضي عند غيابه) | 80 | 81 | ## تشغيل البوت 82 | 83 | - استخدم اﻷمر التالي لبدء البوت باستخدام دوكر: 84 | 85 | ```bash 86 | docker-compose up --build -d 87 | ``` 88 | 89 | ### تشغيل البوت دون دوكر 90 | 91 | #### إعتماديات بايثون 92 | 93 | - يحتاج البوت وجود إصدار بايثون 3.9 أو أحدث، وكذلك pip اﻹصدار 19 أو أحدث أو أداة poetry إذا كنت تستخدمها. 94 | 95 | ##### التثبيت باستخدام poetry 96 | 97 | ```bash 98 | poetry install 99 | ``` 100 | 101 | ##### التثبيت باستخدام Pip 102 | 103 | ```bash 104 | pip install . 105 | ``` 106 | 107 | #### قاعدة البيانات 108 | 109 | يستخدم البوت قاعدة بيانات sqlite. تحقق من أنها مثبتة بنظامك. 110 | 111 | #### تشغيل البوت 112 | 113 | استخدم اﻷمر التالي لبدء البوت 114 | 115 | ```bash 116 | python3 -m droos_bot 117 | ``` 118 | 119 | ## معلومات تقنية 120 | 121 | يستخدم البوت: 122 | 123 | - [Python 3](https://python.org/) 124 | - [Docker](https://www.docker.com/) 125 | 126 | ومكتبات بايثون: 127 | 128 | - [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot/) 129 | - [python-telegram-bot-pagination](https://github.com/ksinn/python-telegram-bot-pagination) 130 | - [https://github.com/burnash/gspread](https://github.com/burnash/gspread) 131 | - [gspread-pandas](https://github.com/aiguofer/gspread-pandas) 132 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "ansicon" 7 | version = "1.89.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312, upload-time = "2019-04-29T20:23:57.314Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.12.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, 24 | ] 25 | 26 | [[package]] 27 | name = "blessed" 28 | version = "1.25.0" 29 | source = { registry = "https://pypi.org/simple" } 30 | dependencies = [ 31 | { name = "jinxed", marker = "sys_platform == 'win32'" }, 32 | { name = "wcwidth" }, 33 | ] 34 | sdist = { url = "https://files.pythonhosted.org/packages/33/cd/eed8b82f1fabcb817d84b24d0780b86600b5c3df7ec4f890bcbb2371b0ad/blessed-1.25.0.tar.gz", hash = "sha256:606aebfea69f85915c7ca6a96eb028e0031d30feccc5688e13fd5cec8277b28d", size = 6746381, upload-time = "2025-11-18T18:43:52.71Z" } 35 | wheels = [ 36 | { url = "https://files.pythonhosted.org/packages/7c/2c/e9b6dd824fb6e76dbd39a308fc6f497320afd455373aac8518ca3eba7948/blessed-1.25.0-py3-none-any.whl", hash = "sha256:e52b9f778b9e10c30b3f17f6b5f5d2208d1e9b53b270f1d94fc61a243fc4708f", size = 95646, upload-time = "2025-11-18T18:43:50.924Z" }, 37 | ] 38 | 39 | [[package]] 40 | name = "cachetools" 41 | version = "6.2.3" 42 | source = { registry = "https://pypi.org/simple" } 43 | sdist = { url = "https://files.pythonhosted.org/packages/b5/44/5dc354b9f2df614673c2a542a630ef95d578b4a8673a1046d1137a7e2453/cachetools-6.2.3.tar.gz", hash = "sha256:64e0a4ddf275041dd01f5b873efa87c91ea49022b844b8c5d1ad3407c0f42f1f", size = 31641, upload-time = "2025-12-12T21:18:06.011Z" } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/ab/de/aa4cfc69feb5b3d604310214369979bb222ed0df0e2575a1b6e7af1a5579/cachetools-6.2.3-py3-none-any.whl", hash = "sha256:3fde34f7033979efb1e79b07ae529c2c40808bdd23b0b731405a48439254fba5", size = 11554, upload-time = "2025-12-12T21:18:04.556Z" }, 46 | ] 47 | 48 | [[package]] 49 | name = "certifi" 50 | version = "2025.11.12" 51 | source = { registry = "https://pypi.org/simple" } 52 | sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } 53 | wheels = [ 54 | { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, 55 | ] 56 | 57 | [[package]] 58 | name = "cfgv" 59 | version = "3.5.0" 60 | source = { registry = "https://pypi.org/simple" } 61 | sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } 62 | wheels = [ 63 | { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, 64 | ] 65 | 66 | [[package]] 67 | name = "charset-normalizer" 68 | version = "3.4.4" 69 | source = { registry = "https://pypi.org/simple" } 70 | sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } 71 | wheels = [ 72 | { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, 73 | { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, 74 | { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, 75 | { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, 76 | { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, 77 | { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, 78 | { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, 79 | { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, 80 | { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, 81 | { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, 82 | { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, 83 | { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, 84 | { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, 85 | { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, 86 | { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, 87 | { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, 88 | { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, 89 | { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, 90 | { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, 91 | { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, 92 | { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, 93 | { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, 94 | { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, 95 | { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, 96 | { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, 97 | { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, 98 | { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, 99 | { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, 100 | { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, 101 | { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, 102 | { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, 103 | { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, 104 | { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, 105 | ] 106 | 107 | [[package]] 108 | name = "codefind" 109 | version = "0.1.7" 110 | source = { registry = "https://pypi.org/simple" } 111 | sdist = { url = "https://files.pythonhosted.org/packages/e5/fb/029e384b0225ea3401fc19f402af63d161462d19091dbf6b50f85cfc4b91/codefind-0.1.7.tar.gz", hash = "sha256:a2ec8a2c0180399ea838dfcdcc344ca89f97b8aa293bc17b22b2c023aba06fbc", size = 20033, upload-time = "2024-09-08T23:30:01.412Z" } 112 | wheels = [ 113 | { url = "https://files.pythonhosted.org/packages/1d/2e/f6403d675c1a99a40d076d62ed9759b8863d1ac8119508e902cb0a33691d/codefind-0.1.7-py3-none-any.whl", hash = "sha256:5f1305b0992185cc87f28925c4449d04a256389099d46a489e619a296a802a29", size = 3861, upload-time = "2024-09-08T23:30:02.273Z" }, 114 | ] 115 | 116 | [[package]] 117 | name = "decorator" 118 | version = "5.2.1" 119 | source = { registry = "https://pypi.org/simple" } 120 | sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } 121 | wheels = [ 122 | { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, 123 | ] 124 | 125 | [[package]] 126 | name = "distlib" 127 | version = "0.4.0" 128 | source = { registry = "https://pypi.org/simple" } 129 | sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } 130 | wheels = [ 131 | { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, 132 | ] 133 | 134 | [[package]] 135 | name = "droos-bot" 136 | version = "1.0.0" 137 | source = { virtual = "." } 138 | dependencies = [ 139 | { name = "gspread" }, 140 | { name = "gspread-pandas" }, 141 | { name = "pyarabic" }, 142 | { name = "python-telegram-bot" }, 143 | { name = "sqlalchemy" }, 144 | ] 145 | 146 | [package.dev-dependencies] 147 | dev = [ 148 | { name = "jurigged" }, 149 | { name = "pre-commit" }, 150 | { name = "ruff" }, 151 | ] 152 | 153 | [package.metadata] 154 | requires-dist = [ 155 | { name = "gspread", specifier = ">=5.12.4" }, 156 | { name = "gspread-pandas", specifier = ">=3.3.0" }, 157 | { name = "pyarabic", specifier = ">=0.6.15" }, 158 | { name = "python-telegram-bot", specifier = ">=21.4" }, 159 | { name = "sqlalchemy", specifier = ">=2.0.31" }, 160 | ] 161 | 162 | [package.metadata.requires-dev] 163 | dev = [ 164 | { name = "jurigged", specifier = ">=0.6.0" }, 165 | { name = "pre-commit", specifier = ">=4.0.0" }, 166 | { name = "ruff", specifier = ">=0.8.1" }, 167 | ] 168 | 169 | [[package]] 170 | name = "filelock" 171 | version = "3.20.0" 172 | source = { registry = "https://pypi.org/simple" } 173 | sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } 174 | wheels = [ 175 | { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, 176 | ] 177 | 178 | [[package]] 179 | name = "google-auth" 180 | version = "2.43.0" 181 | source = { registry = "https://pypi.org/simple" } 182 | dependencies = [ 183 | { name = "cachetools" }, 184 | { name = "pyasn1-modules" }, 185 | { name = "rsa" }, 186 | ] 187 | sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } 188 | wheels = [ 189 | { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, 190 | ] 191 | 192 | [[package]] 193 | name = "google-auth-oauthlib" 194 | version = "1.2.2" 195 | source = { registry = "https://pypi.org/simple" } 196 | dependencies = [ 197 | { name = "google-auth" }, 198 | { name = "requests-oauthlib" }, 199 | ] 200 | sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" } 201 | wheels = [ 202 | { url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" }, 203 | ] 204 | 205 | [[package]] 206 | name = "greenlet" 207 | version = "3.3.0" 208 | source = { registry = "https://pypi.org/simple" } 209 | sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, 212 | { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, 213 | { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, 214 | { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, 215 | { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, 216 | { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, 217 | { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, 218 | { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, 219 | { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, 220 | { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, 221 | { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, 222 | { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, 223 | { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, 224 | { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, 225 | { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, 226 | { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, 227 | { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, 228 | { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, 229 | { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, 230 | { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, 231 | { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, 232 | { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, 233 | { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, 234 | ] 235 | 236 | [[package]] 237 | name = "gspread" 238 | version = "5.12.4" 239 | source = { registry = "https://pypi.org/simple" } 240 | dependencies = [ 241 | { name = "google-auth" }, 242 | { name = "google-auth-oauthlib" }, 243 | ] 244 | sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/8e822fcdb4c1237399cd4a3c8d235f02bad897d81f64f587fbee3e543e88/gspread-5.12.4.tar.gz", hash = "sha256:3fcef90183f15d3c9233b4caa021a83682f2b2ee678340c42d7ca7d8be98c6d1", size = 69868, upload-time = "2023-12-31T12:48:37.944Z" } 245 | wheels = [ 246 | { url = "https://files.pythonhosted.org/packages/22/25/7457afe296c3c7ad6ec374c91ecefe3da2c6585fa4b2818e00ee3546a33d/gspread-5.12.4-py3-none-any.whl", hash = "sha256:1e453d87e0fde23bc5546e33eb684cf8b8c26540615f2f1ae004a9084a29051d", size = 49511, upload-time = "2023-12-31T12:48:36.212Z" }, 247 | ] 248 | 249 | [[package]] 250 | name = "gspread-pandas" 251 | version = "3.3.0" 252 | source = { registry = "https://pypi.org/simple" } 253 | dependencies = [ 254 | { name = "decorator" }, 255 | { name = "google-auth" }, 256 | { name = "google-auth-oauthlib" }, 257 | { name = "gspread" }, 258 | { name = "pandas" }, 259 | ] 260 | sdist = { url = "https://files.pythonhosted.org/packages/5e/5c/851abfb9adf4e70b232f3b6e0237e416776aae9257b59eaa0f44c0960084/gspread-pandas-3.3.0.tar.gz", hash = "sha256:aac84bd63594db6271ad2cfe10be64614ea5d1129d063ca57b40c2b9dcc18013", size = 29525, upload-time = "2024-02-13T17:02:55.886Z" } 261 | wheels = [ 262 | { url = "https://files.pythonhosted.org/packages/55/40/8839c83d13d31687e28d2eed21ae66f77889dcdbfcedc71ae6a0aa93863f/gspread_pandas-3.3.0-py2.py3-none-any.whl", hash = "sha256:a4dfddd7b1c5418742e30099a01766ecd96d1f5bbcad8b1e1060c4a5f16fd627", size = 27301, upload-time = "2024-02-13T17:02:52.997Z" }, 263 | ] 264 | 265 | [[package]] 266 | name = "h11" 267 | version = "0.16.0" 268 | source = { registry = "https://pypi.org/simple" } 269 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 270 | wheels = [ 271 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 272 | ] 273 | 274 | [[package]] 275 | name = "httpcore" 276 | version = "1.0.9" 277 | source = { registry = "https://pypi.org/simple" } 278 | dependencies = [ 279 | { name = "certifi" }, 280 | { name = "h11" }, 281 | ] 282 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 285 | ] 286 | 287 | [[package]] 288 | name = "httpx" 289 | version = "0.28.1" 290 | source = { registry = "https://pypi.org/simple" } 291 | dependencies = [ 292 | { name = "anyio" }, 293 | { name = "certifi" }, 294 | { name = "httpcore" }, 295 | { name = "idna" }, 296 | ] 297 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 298 | wheels = [ 299 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 300 | ] 301 | 302 | [[package]] 303 | name = "identify" 304 | version = "2.6.15" 305 | source = { registry = "https://pypi.org/simple" } 306 | sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } 307 | wheels = [ 308 | { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, 309 | ] 310 | 311 | [[package]] 312 | name = "idna" 313 | version = "3.11" 314 | source = { registry = "https://pypi.org/simple" } 315 | sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } 316 | wheels = [ 317 | { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, 318 | ] 319 | 320 | [[package]] 321 | name = "jinxed" 322 | version = "1.3.0" 323 | source = { registry = "https://pypi.org/simple" } 324 | dependencies = [ 325 | { name = "ansicon", marker = "sys_platform == 'win32'" }, 326 | ] 327 | sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981, upload-time = "2024-07-31T22:39:18.854Z" } 328 | wheels = [ 329 | { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085, upload-time = "2024-07-31T22:39:17.426Z" }, 330 | ] 331 | 332 | [[package]] 333 | name = "jurigged" 334 | version = "0.6.1" 335 | source = { registry = "https://pypi.org/simple" } 336 | dependencies = [ 337 | { name = "blessed" }, 338 | { name = "codefind" }, 339 | { name = "ovld" }, 340 | { name = "watchdog" }, 341 | ] 342 | sdist = { url = "https://files.pythonhosted.org/packages/2a/25/15c05f8425397a1cd700e5a102fed9a7889c7644dd5ee37b3b7939b9539c/jurigged-0.6.1.tar.gz", hash = "sha256:840a53bee6963a85d0a4ea5055472526c6b1114ed78ef5a91bde146e9280b04b", size = 63750, upload-time = "2025-05-13T22:29:29.503Z" } 343 | wheels = [ 344 | { url = "https://files.pythonhosted.org/packages/3c/9c/e284364201260efb4a6a2f9ac57e8faba2e9046ea3a4c7e62b0c84775095/jurigged-0.6.1-py3-none-any.whl", hash = "sha256:2479f1e7463c29639ac7764672e2709391ff0910b3cf27125a7985f2a7b04355", size = 37445, upload-time = "2025-05-13T22:29:28.19Z" }, 345 | ] 346 | 347 | [[package]] 348 | name = "nodeenv" 349 | version = "1.9.1" 350 | source = { registry = "https://pypi.org/simple" } 351 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 352 | wheels = [ 353 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 354 | ] 355 | 356 | [[package]] 357 | name = "numpy" 358 | version = "2.3.5" 359 | source = { registry = "https://pypi.org/simple" } 360 | sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } 361 | wheels = [ 362 | { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, 363 | { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, 364 | { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, 365 | { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, 366 | { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, 367 | { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, 368 | { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, 369 | { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, 370 | { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, 371 | { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, 372 | { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, 373 | { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, 374 | { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, 375 | { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, 376 | { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, 377 | { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, 378 | { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, 379 | { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, 380 | { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, 381 | { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, 382 | { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, 383 | { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, 384 | { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, 385 | { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, 386 | { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, 387 | { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, 388 | { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, 389 | { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, 390 | { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, 391 | { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, 392 | { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, 393 | { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, 394 | { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, 395 | { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, 396 | { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, 397 | { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, 398 | { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, 399 | { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, 400 | { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, 401 | { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, 402 | { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, 403 | { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, 404 | { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, 405 | { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, 406 | ] 407 | 408 | [[package]] 409 | name = "oauthlib" 410 | version = "3.3.1" 411 | source = { registry = "https://pypi.org/simple" } 412 | sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } 413 | wheels = [ 414 | { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, 415 | ] 416 | 417 | [[package]] 418 | name = "ovld" 419 | version = "0.5.13" 420 | source = { registry = "https://pypi.org/simple" } 421 | sdist = { url = "https://files.pythonhosted.org/packages/bd/e5/321d78f42185be4220b05f2dbd85ff16235c8c422aae9ef60a7f792a7950/ovld-0.5.13.tar.gz", hash = "sha256:923753b27558a328bef1565c3e5911757319bac5147a1a76a1cd598bc76e2db1", size = 105030, upload-time = "2025-09-11T16:11:32.67Z" } 422 | wheels = [ 423 | { url = "https://files.pythonhosted.org/packages/90/84/adb3bac692d889bfd6738ae46197997799f89965a35524a1e8de4c3b568e/ovld-0.5.13-py3-none-any.whl", hash = "sha256:9e625e8f74aa028e134372472894bb0327443c03dcb6691aeb721c5a7afd0bfd", size = 39779, upload-time = "2025-09-11T16:11:30.979Z" }, 424 | ] 425 | 426 | [[package]] 427 | name = "pandas" 428 | version = "2.3.3" 429 | source = { registry = "https://pypi.org/simple" } 430 | dependencies = [ 431 | { name = "numpy" }, 432 | { name = "python-dateutil" }, 433 | { name = "pytz" }, 434 | { name = "tzdata" }, 435 | ] 436 | sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } 437 | wheels = [ 438 | { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, 439 | { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, 440 | { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, 441 | { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, 442 | { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, 443 | { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, 444 | { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, 445 | { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, 446 | { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, 447 | { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, 448 | { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, 449 | { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, 450 | { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, 451 | { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, 452 | { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, 453 | { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, 454 | { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, 455 | { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, 456 | { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, 457 | { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, 458 | { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, 459 | { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, 460 | { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, 461 | { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, 462 | { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, 463 | { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, 464 | ] 465 | 466 | [[package]] 467 | name = "platformdirs" 468 | version = "4.5.1" 469 | source = { registry = "https://pypi.org/simple" } 470 | sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } 471 | wheels = [ 472 | { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, 473 | ] 474 | 475 | [[package]] 476 | name = "pre-commit" 477 | version = "4.5.0" 478 | source = { registry = "https://pypi.org/simple" } 479 | dependencies = [ 480 | { name = "cfgv" }, 481 | { name = "identify" }, 482 | { name = "nodeenv" }, 483 | { name = "pyyaml" }, 484 | { name = "virtualenv" }, 485 | ] 486 | sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } 487 | wheels = [ 488 | { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, 489 | ] 490 | 491 | [[package]] 492 | name = "pyarabic" 493 | version = "0.6.15" 494 | source = { registry = "https://pypi.org/simple" } 495 | dependencies = [ 496 | { name = "six" }, 497 | ] 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/d7/64/0ea5be39e6a6515804cae8c280226d771f42750a08182f9d2e5f3b822694/PyArabic-0.6.15-py3-none-any.whl", hash = "sha256:b9a530277876008f5fbe53249c6953b4513dbbf2ea8f4339694e87c8a75d7edf", size = 126368, upload-time = "2022-06-18T10:47:16.66Z" }, 500 | ] 501 | 502 | [[package]] 503 | name = "pyasn1" 504 | version = "0.6.1" 505 | source = { registry = "https://pypi.org/simple" } 506 | sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } 507 | wheels = [ 508 | { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, 509 | ] 510 | 511 | [[package]] 512 | name = "pyasn1-modules" 513 | version = "0.4.2" 514 | source = { registry = "https://pypi.org/simple" } 515 | dependencies = [ 516 | { name = "pyasn1" }, 517 | ] 518 | sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } 519 | wheels = [ 520 | { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, 521 | ] 522 | 523 | [[package]] 524 | name = "python-dateutil" 525 | version = "2.9.0.post0" 526 | source = { registry = "https://pypi.org/simple" } 527 | dependencies = [ 528 | { name = "six" }, 529 | ] 530 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } 531 | wheels = [ 532 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, 533 | ] 534 | 535 | [[package]] 536 | name = "python-telegram-bot" 537 | version = "22.5" 538 | source = { registry = "https://pypi.org/simple" } 539 | dependencies = [ 540 | { name = "httpx" }, 541 | ] 542 | sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1c519a3ca8ad0babc650ec63dbfbd1b73babf625ed54/python_telegram_bot-22.5.tar.gz", hash = "sha256:82d4efd891d04132f308f0369f5b5929e0b96957901f58bcef43911c5f6f92f8", size = 1488269, upload-time = "2025-09-27T13:50:27.879Z" } 543 | wheels = [ 544 | { url = "https://files.pythonhosted.org/packages/bc/c3/340c7520095a8c79455fcf699cbb207225e5b36490d2b9ee557c16a7b21b/python_telegram_bot-22.5-py3-none-any.whl", hash = "sha256:4b7cd365344a7dce54312cc4520d7fa898b44d1a0e5f8c74b5bd9b540d035d16", size = 730976, upload-time = "2025-09-27T13:50:25.93Z" }, 545 | ] 546 | 547 | [[package]] 548 | name = "pytz" 549 | version = "2025.2" 550 | source = { registry = "https://pypi.org/simple" } 551 | sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } 552 | wheels = [ 553 | { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, 554 | ] 555 | 556 | [[package]] 557 | name = "pyyaml" 558 | version = "6.0.3" 559 | source = { registry = "https://pypi.org/simple" } 560 | sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } 561 | wheels = [ 562 | { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, 563 | { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, 564 | { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, 565 | { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, 566 | { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, 567 | { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, 568 | { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, 569 | { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, 570 | { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, 571 | { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, 572 | { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, 573 | { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, 574 | { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, 575 | { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, 576 | { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, 577 | { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, 578 | { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, 579 | { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, 580 | { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, 581 | { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, 582 | { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, 583 | { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, 584 | { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, 585 | { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, 586 | { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, 587 | { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, 588 | { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, 589 | { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, 590 | ] 591 | 592 | [[package]] 593 | name = "requests" 594 | version = "2.32.5" 595 | source = { registry = "https://pypi.org/simple" } 596 | dependencies = [ 597 | { name = "certifi" }, 598 | { name = "charset-normalizer" }, 599 | { name = "idna" }, 600 | { name = "urllib3" }, 601 | ] 602 | sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } 603 | wheels = [ 604 | { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, 605 | ] 606 | 607 | [[package]] 608 | name = "requests-oauthlib" 609 | version = "2.0.0" 610 | source = { registry = "https://pypi.org/simple" } 611 | dependencies = [ 612 | { name = "oauthlib" }, 613 | { name = "requests" }, 614 | ] 615 | sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } 616 | wheels = [ 617 | { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, 618 | ] 619 | 620 | [[package]] 621 | name = "rsa" 622 | version = "4.9.1" 623 | source = { registry = "https://pypi.org/simple" } 624 | dependencies = [ 625 | { name = "pyasn1" }, 626 | ] 627 | sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } 628 | wheels = [ 629 | { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, 630 | ] 631 | 632 | [[package]] 633 | name = "ruff" 634 | version = "0.14.9" 635 | source = { registry = "https://pypi.org/simple" } 636 | sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } 637 | wheels = [ 638 | { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, 639 | { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, 640 | { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, 641 | { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, 642 | { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, 643 | { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, 644 | { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, 645 | { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, 646 | { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, 647 | { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, 648 | { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, 649 | { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, 650 | { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, 651 | { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, 652 | { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, 653 | { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, 654 | { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, 655 | { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, 656 | ] 657 | 658 | [[package]] 659 | name = "six" 660 | version = "1.17.0" 661 | source = { registry = "https://pypi.org/simple" } 662 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } 663 | wheels = [ 664 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, 665 | ] 666 | 667 | [[package]] 668 | name = "sqlalchemy" 669 | version = "2.0.45" 670 | source = { registry = "https://pypi.org/simple" } 671 | dependencies = [ 672 | { 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'" }, 673 | { name = "typing-extensions" }, 674 | ] 675 | sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } 676 | wheels = [ 677 | { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, 678 | { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, 679 | { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, 680 | { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, 681 | { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, 682 | { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, 683 | { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, 684 | { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, 685 | { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, 686 | { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, 687 | { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, 688 | { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, 689 | { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, 690 | { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, 691 | { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, 692 | { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, 693 | { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, 694 | ] 695 | 696 | [[package]] 697 | name = "typing-extensions" 698 | version = "4.15.0" 699 | source = { registry = "https://pypi.org/simple" } 700 | 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" } 701 | wheels = [ 702 | { 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" }, 703 | ] 704 | 705 | [[package]] 706 | name = "tzdata" 707 | version = "2025.2" 708 | source = { registry = "https://pypi.org/simple" } 709 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } 710 | wheels = [ 711 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, 712 | ] 713 | 714 | [[package]] 715 | name = "urllib3" 716 | version = "2.6.2" 717 | source = { registry = "https://pypi.org/simple" } 718 | sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } 719 | wheels = [ 720 | { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, 721 | ] 722 | 723 | [[package]] 724 | name = "virtualenv" 725 | version = "20.35.4" 726 | source = { registry = "https://pypi.org/simple" } 727 | dependencies = [ 728 | { name = "distlib" }, 729 | { name = "filelock" }, 730 | { name = "platformdirs" }, 731 | ] 732 | sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } 733 | wheels = [ 734 | { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, 735 | ] 736 | 737 | [[package]] 738 | name = "watchdog" 739 | version = "6.0.0" 740 | source = { registry = "https://pypi.org/simple" } 741 | sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } 742 | wheels = [ 743 | { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, 744 | { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, 745 | { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, 746 | { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, 747 | { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, 748 | { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, 749 | { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, 750 | { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, 751 | { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, 752 | { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, 753 | { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, 754 | { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, 755 | { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, 756 | ] 757 | 758 | [[package]] 759 | name = "wcwidth" 760 | version = "0.2.14" 761 | source = { registry = "https://pypi.org/simple" } 762 | sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } 763 | wheels = [ 764 | { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, 765 | ] 766 | --------------------------------------------------------------------------------