From be9ce535613ee248d4bd7073781fadcb780a7469 Mon Sep 17 00:00:00 2001 From: dnslin Date: Fri, 12 Dec 2025 15:09:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0OneDrive=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 22 +++ pyproject.toml | 1 + src/cloud/__init__.py | 11 ++ src/cloud/base.py | 91 ++++++++++++ src/cloud/onedrive.py | 230 +++++++++++++++++++++++++++++ src/core/config.py | 26 ++++ src/core/constants.py | 3 + src/core/exceptions.py | 12 ++ src/telegram/app.py | 5 +- src/telegram/handlers.py | 293 ++++++++++++++++++++++++++++++++++++- src/telegram/keyboards.py | 59 ++++++++ uv.lock | 295 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 1045 insertions(+), 3 deletions(-) create mode 100644 src/cloud/__init__.py create mode 100644 src/cloud/base.py create mode 100644 src/cloud/onedrive.py diff --git a/.env.example b/.env.example index fde0787..a0e0778 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,25 @@ ARIA2_RPC_PORT=6800 # Aria2 RPC Secret (optional, auto-generated if empty) ARIA2_RPC_SECRET= + +# ==================== OneDrive 配置 ==================== +# 启用 OneDrive 云存储功能 +ONEDRIVE_ENABLED=false + +# Microsoft Azure 应用凭证 +# 在 Azure Portal 创建应用:https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade +# 需要添加 API 权限:Files.ReadWrite.All, offline_access +ONEDRIVE_CLIENT_ID= +ONEDRIVE_CLIENT_SECRET= + +# 租户 ID(个人账户使用 common,组织账户使用具体租户 ID) +ONEDRIVE_TENANT_ID=common + +# 下载完成后自动上传到 OneDrive +ONEDRIVE_AUTO_UPLOAD=false + +# 上传完成后删除本地文件 +ONEDRIVE_DELETE_AFTER_UPLOAD=false + +# OneDrive 远程存储路径 +ONEDRIVE_REMOTE_PATH=/aria2bot diff --git a/pyproject.toml b/pyproject.toml index 9f72808..a2888f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "python-dotenv>=1.2.1", "python-telegram-bot>=21.0", "httpx>=0.27.0", + "O365>=2.0.0", ] [project.scripts] diff --git a/src/cloud/__init__.py b/src/cloud/__init__.py new file mode 100644 index 0000000..75df2eb --- /dev/null +++ b/src/cloud/__init__.py @@ -0,0 +1,11 @@ +"""云存储模块""" +from src.cloud.base import CloudStorageBase, UploadProgress, UploadStatus + +__all__ = ["CloudStorageBase", "UploadProgress", "UploadStatus"] + +# OneDriveClient 延迟导入,避免在 O365 未安装时报错 +try: + from src.cloud.onedrive import OneDriveClient + __all__.append("OneDriveClient") +except ImportError: + OneDriveClient = None diff --git a/src/cloud/base.py b/src/cloud/base.py new file mode 100644 index 0000000..91584de --- /dev/null +++ b/src/cloud/base.py @@ -0,0 +1,91 @@ +"""云存储抽象基类""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Callable + + +class UploadStatus(Enum): + """上传状态""" + PENDING = "pending" + UPLOADING = "uploading" + COMPLETED = "completed" + FAILED = "failed" + + +@dataclass +class UploadProgress: + """上传进度""" + file_name: str + total_size: int + uploaded_size: int + status: UploadStatus + error_message: str = "" + + @property + def progress(self) -> float: + """计算上传进度百分比""" + if self.total_size == 0: + return 0.0 + return (self.uploaded_size / self.total_size) * 100 + + +class CloudStorageBase(ABC): + """云存储抽象基类,用于扩展不同云存储服务""" + + @abstractmethod + async def is_authenticated(self) -> bool: + """检查是否已认证""" + pass + + @abstractmethod + async def get_auth_url(self) -> tuple[str, str]: + """获取认证 URL + + Returns: + tuple[str, str]: (认证URL, state) + """ + pass + + @abstractmethod + async def authenticate_with_code(self, callback_url: str) -> bool: + """使用回调 URL 完成认证 + + Args: + callback_url: 授权后的回调 URL + + Returns: + bool: 认证是否成功 + """ + pass + + @abstractmethod + async def upload_file( + self, + local_path: Path, + remote_path: str, + progress_callback: Callable[[UploadProgress], None] | None = None + ) -> bool: + """上传文件 + + Args: + local_path: 本地文件路径 + remote_path: 远程目录路径 + progress_callback: 进度回调函数 + + Returns: + bool: 上传是否成功 + """ + pass + + @abstractmethod + async def logout(self) -> bool: + """登出/清除认证 + + Returns: + bool: 登出是否成功 + """ + pass diff --git a/src/cloud/onedrive.py b/src/cloud/onedrive.py new file mode 100644 index 0000000..98b39d9 --- /dev/null +++ b/src/cloud/onedrive.py @@ -0,0 +1,230 @@ +"""OneDrive 云存储客户端""" +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from typing import Callable + +from O365 import Account +from O365.utils import BaseTokenBackend + +from src.cloud.base import CloudStorageBase, UploadProgress, UploadStatus +from src.core.config import OneDriveConfig +from src.core.constants import CLOUD_TOKEN_DIR +from src.utils.logger import get_logger + +logger = get_logger("onedrive") + + +class FileTokenBackend(BaseTokenBackend): + """文件系统 Token 存储后端""" + + def __init__(self, token_path: Path): + super().__init__() + self.token_path = token_path + + def load_token(self): + """从文件加载 Token""" + if self.token_path.exists(): + try: + token_data = self.deserialize(self.token_path.read_text()) + self._cache = token_data + return True + except (json.JSONDecodeError, OSError) as e: + logger.warning(f"加载 Token 失败: {e}") + return False + + def save_token(self, force=False): + """保存 Token 到文件""" + try: + self.token_path.parent.mkdir(parents=True, exist_ok=True) + self.token_path.write_text(self.serialize()) + return True + except OSError as e: + logger.error(f"保存 Token 失败: {e}") + return False + + def delete_token(self): + """删除 Token 文件""" + try: + if self.token_path.exists(): + self.token_path.unlink() + self._cache = {} + return True + except OSError as e: + logger.error(f"删除 Token 失败: {e}") + return False + + def check_token(self): + """检查 Token 是否存在""" + return self.token_path.exists() and self.has_data + + +class OneDriveClient(CloudStorageBase): + """OneDrive 客户端实现""" + + # OneDrive 所需的权限范围 + SCOPES = ["Files.ReadWrite", "offline_access"] + + def __init__(self, config: OneDriveConfig): + self.config = config + self._account: Account | None = None + self._token_backend = FileTokenBackend(CLOUD_TOKEN_DIR / "onedrive_token.json") + + def _get_account(self) -> Account: + """获取或创建 Account 实例""" + if self._account is None: + # 公共客户端只需要 client_id,不需要 client_secret + credentials = (self.config.client_id,) + self._account = Account( + credentials, + auth_flow_type="public", + tenant_id=self.config.tenant_id, + token_backend=self._token_backend, + scopes=self.SCOPES, + ) + return self._account + + async def is_authenticated(self) -> bool: + """检查是否已认证""" + account = self._get_account() + return account.is_authenticated + + async def get_auth_url(self) -> tuple[str, str]: + """获取认证 URL""" + account = self._get_account() + # MSAL 保留的 scope 不能传入,会自动处理 + reserved = {"openid", "offline_access", "profile"} + scopes = [s for s in self.SCOPES if s not in reserved] + redirect_uri = "https://login.microsoftonline.com/common/oauth2/nativeclient" + url, state = account.con.get_authorization_url( + requested_scopes=scopes, + redirect_uri=redirect_uri + ) + return url, state + + async def authenticate_with_code(self, callback_url: str, flow: dict | None = None) -> bool: + """使用回调 URL 完成认证""" + account = self._get_account() + try: + redirect_uri = "https://login.microsoftonline.com/common/oauth2/nativeclient" + result = account.con.request_token( + callback_url, + redirect_uri=redirect_uri, + flow=flow + ) + if result: + logger.info("OneDrive 认证成功") + return bool(result) + except Exception as e: + import traceback + logger.error(f"OneDrive 认证失败: {e}\n{traceback.format_exc()}") + return False + + async def upload_file( + self, + local_path: Path, + remote_path: str, + progress_callback: Callable[[UploadProgress], None] | None = None + ) -> bool: + """上传文件到 OneDrive""" + account = self._get_account() + if not account.is_authenticated: + raise RuntimeError("OneDrive 未认证") + + try: + # 获取存储和驱动器 + storage = account.storage() + drive = storage.get_default_drive() + root = drive.get_root_folder() + + # 确保远程目录存在 + target_folder = await self._ensure_folder_path(root, remote_path) + + file_size = local_path.stat().st_size + file_name = local_path.name + + # 发送上传开始通知 + if progress_callback: + progress_callback(UploadProgress( + file_name=file_name, + total_size=file_size, + uploaded_size=0, + status=UploadStatus.UPLOADING + )) + + # 执行上传(python-o365 会自动处理大文件分块) + uploaded = await asyncio.to_thread( + target_folder.upload_file, + item=str(local_path) + ) + + if uploaded: + if progress_callback: + progress_callback(UploadProgress( + file_name=file_name, + total_size=file_size, + uploaded_size=file_size, + status=UploadStatus.COMPLETED + )) + logger.info(f"文件上传成功: {file_name}") + return True + + return False + + except Exception as e: + logger.error(f"上传失败: {e}") + if progress_callback: + progress_callback(UploadProgress( + file_name=local_path.name, + total_size=0, + uploaded_size=0, + status=UploadStatus.FAILED, + error_message=str(e) + )) + return False + + async def _ensure_folder_path(self, root_folder, path: str): + """确保远程目录路径存在,不存在则创建 + + Args: + root_folder: OneDrive 根文件夹对象 + path: 目标路径,如 "/aria2bot/downloads" + + Returns: + 目标文件夹对象 + """ + parts = [p for p in path.strip("/").split("/") if p] + current = root_folder + + for part in parts: + # 查找子文件夹 + items = await asyncio.to_thread(lambda: list(current.get_items())) + found = None + for item in items: + if item.is_folder and item.name == part: + found = item + break + + if found: + current = found + else: + # 创建新文件夹 + current = await asyncio.to_thread( + current.create_child_folder, part + ) + logger.info(f"创建远程目录: {part}") + + return current + + async def logout(self) -> bool: + """清除认证""" + try: + self._token_backend.delete_token() + self._account = None + logger.info("OneDrive 已登出") + return True + except Exception as e: + logger.error(f"登出失败: {e}") + return False diff --git a/src/core/config.py b/src/core/config.py index 281bf9f..094f083 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -18,12 +18,25 @@ class Aria2Config: bt_tracker_update: bool = True +@dataclass +class OneDriveConfig: + """OneDrive 配置""" + enabled: bool = False + client_id: str = "" + client_secret: str = "" + tenant_id: str = "common" + auto_upload: bool = False + delete_after_upload: bool = False + remote_path: str = "/aria2bot" + + @dataclass class BotConfig: token: str = "" api_base_url: str = "" allowed_users: set[int] = field(default_factory=set) aria2: Aria2Config = field(default_factory=Aria2Config) + onedrive: OneDriveConfig = field(default_factory=OneDriveConfig) @classmethod def from_env(cls) -> "BotConfig": @@ -62,9 +75,22 @@ class BotConfig: rpc_port=rpc_port, rpc_secret=os.environ.get("ARIA2_RPC_SECRET", ""), ) + + # 解析 OneDrive 配置 + onedrive = OneDriveConfig( + enabled=os.environ.get("ONEDRIVE_ENABLED", "").lower() == "true", + client_id=os.environ.get("ONEDRIVE_CLIENT_ID", ""), + client_secret=os.environ.get("ONEDRIVE_CLIENT_SECRET", ""), + tenant_id=os.environ.get("ONEDRIVE_TENANT_ID", "common"), + auto_upload=os.environ.get("ONEDRIVE_AUTO_UPLOAD", "").lower() == "true", + delete_after_upload=os.environ.get("ONEDRIVE_DELETE_AFTER_UPLOAD", "").lower() == "true", + remote_path=os.environ.get("ONEDRIVE_REMOTE_PATH", "/aria2bot"), + ) + return cls( token=token, api_base_url=os.environ.get("TELEGRAM_API_BASE_URL", ""), allowed_users=allowed_users, aria2=aria2, + onedrive=onedrive, ) diff --git a/src/core/constants.py b/src/core/constants.py index acafddf..bceda48 100644 --- a/src/core/constants.py +++ b/src/core/constants.py @@ -12,3 +12,6 @@ ARIA2_DHT6 = ARIA2_CONFIG_DIR / "dht6.dat" DOWNLOAD_DIR = HOME / "downloads" SYSTEMD_USER_DIR = HOME / ".config" / "systemd" / "user" ARIA2_SERVICE = SYSTEMD_USER_DIR / "aria2.service" + +# 云存储相关路径 +CLOUD_TOKEN_DIR = ARIA2_CONFIG_DIR / "cloud_tokens" diff --git a/src/core/exceptions.py b/src/core/exceptions.py index 5469fe3..db915c0 100644 --- a/src/core/exceptions.py +++ b/src/core/exceptions.py @@ -31,3 +31,15 @@ class NotInstalledError(Aria2Error): class RpcError(Aria2Error): """RPC 调用失败""" + + +class CloudStorageError(Aria2Error): + """云存储错误基类""" + + +class CloudAuthError(CloudStorageError): + """云存储认证错误""" + + +class CloudUploadError(CloudStorageError): + """云存储上传错误""" diff --git a/src/telegram/app.py b/src/telegram/app.py index d7e0bb4..d1d7658 100644 --- a/src/telegram/app.py +++ b/src/telegram/app.py @@ -28,11 +28,14 @@ BOT_COMMANDS = [ BotCommand("add", "添加下载任务"), BotCommand("list", "查看下载列表"), BotCommand("stats", "全局下载统计"), + BotCommand("cloud", "云存储管理"), ] async def post_init(application: Application) -> None: """应用初始化后设置命令菜单""" + logger = setup_logger() + logger.info("Setting bot commands...") await application.bot.set_my_commands(BOT_COMMANDS) @@ -43,7 +46,7 @@ def create_app(config: BotConfig) -> Application: builder = builder.base_url(config.api_base_url).base_file_url(config.api_base_url + "/file") app = builder.build() - api = Aria2BotAPI(config.aria2, config.allowed_users) + api = Aria2BotAPI(config.aria2, config.allowed_users, config.onedrive) for handler in build_handlers(api): app.add_handler(handler) diff --git a/src/telegram/handlers.py b/src/telegram/handlers.py index c5917de..4dad7cd 100644 --- a/src/telegram/handlers.py +++ b/src/telegram/handlers.py @@ -20,7 +20,9 @@ from src.core import ( get_aria2_version, generate_rpc_secret, ARIA2_CONF, + DOWNLOAD_DIR, ) +from src.core.config import OneDriveConfig from src.aria2 import Aria2Installer, Aria2ServiceManager from src.aria2.rpc import Aria2RpcClient, DownloadTask, _format_size from src.telegram.keyboards import ( @@ -32,6 +34,10 @@ from src.telegram.keyboards import ( build_detail_keyboard, build_after_add_keyboard, build_main_reply_keyboard, + build_cloud_menu_keyboard, + build_upload_choice_keyboard, + build_cloud_settings_keyboard, + build_detail_keyboard_with_upload, ) # Reply Keyboard 按钮文本到命令的映射 @@ -83,13 +89,18 @@ import asyncio from functools import wraps class Aria2BotAPI: - def __init__(self, config: Aria2Config | None = None, allowed_users: set[int] | None = None): + def __init__(self, config: Aria2Config | None = None, allowed_users: set[int] | None = None, + onedrive_config: OneDriveConfig | None = None): self.config = config or Aria2Config() self.allowed_users = allowed_users or set() self.installer = Aria2Installer(self.config) self.service = Aria2ServiceManager() self._rpc: Aria2RpcClient | None = None self._auto_refresh_tasks: dict[str, asyncio.Task] = {} # chat_id:msg_id -> task + # 云存储相关 + self._onedrive_config = onedrive_config + self._onedrive = None + self._pending_auth: dict[int, dict] = {} # user_id -> flow async def _check_permission(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: """检查用户权限,返回 True 表示有权限""" @@ -113,6 +124,13 @@ class Aria2BotAPI: self._rpc = Aria2RpcClient(port=port, secret=secret) return self._rpc + def _get_onedrive_client(self): + """获取或创建 OneDrive 客户端""" + if self._onedrive is None and self._onedrive_config and self._onedrive_config.enabled: + from src.cloud.onedrive import OneDriveClient + self._onedrive = OneDriveClient(self._onedrive_config) + return self._onedrive + async def _reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE, text: str, **kwargs): if update.effective_message: return await update.effective_message.reply_text(text, **kwargs) @@ -120,6 +138,19 @@ class Aria2BotAPI: return await context.bot.send_message(chat_id=update.effective_chat.id, text=text, **kwargs) return None + async def _delayed_delete_messages(self, messages: list, delay: int = 5) -> None: + """延迟删除多条消息""" + try: + await asyncio.sleep(delay) + for msg in messages: + try: + await msg.delete() + except Exception as e: + logger.warning(f"删除消息失败: {e}") + logger.debug("已删除敏感认证消息") + except Exception as e: + logger.warning(f"延迟删除任务失败: {e}") + def _get_rpc_secret(self) -> str: if self.config.rpc_secret: return self.config.rpc_secret @@ -378,6 +409,9 @@ class Aria2BotAPI: "/list - 查看下载列表", "/stats - 全局下载统计", "", + "*云存储*", + "/cloud - 云存储管理菜单", + "", "/menu - 显示快捷菜单", "/help - 显示此帮助", ] @@ -394,6 +428,169 @@ class Aria2BotAPI: reply_markup=keyboard ) + # === 云存储命令 === + + async def cloud_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """云存储管理菜单""" + logger.info(f"收到 /cloud 命令 - {_get_user_info(update)}") + if not self._onedrive_config or not self._onedrive_config.enabled: + await self._reply(update, context, "❌ 云存储功能未启用,请在配置中设置 ONEDRIVE_ENABLED=true") + return + keyboard = build_cloud_menu_keyboard() + await self._reply(update, context, "☁️ *云存储管理*", parse_mode="Markdown", reply_markup=keyboard) + + async def cloud_auth(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """开始 OneDrive 认证""" + logger.info(f"收到云存储认证请求 - {_get_user_info(update)}") + client = self._get_onedrive_client() + if not client: + await self._reply(update, context, "❌ OneDrive 未配置") + return + + if await client.is_authenticated(): + await self._reply(update, context, "✅ OneDrive 已认证") + return + + url, state = await client.get_auth_url() + user_id = update.effective_user.id + + auth_message = await self._reply( + update, context, + f"🔐 *OneDrive 认证*\n\n" + f"1\\. 点击下方链接登录 Microsoft 账户\n" + f"2\\. 授权后会跳转到一个空白页面\n" + f"3\\. 复制该页面的完整 URL 发送给我\n\n" + f"[点击认证]({url})", + parse_mode="Markdown" + ) + self._pending_auth[user_id] = {"state": state, "message": auth_message} + + async def handle_auth_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """处理用户发送的认证回调 URL""" + text = update.message.text + if not text or not text.startswith("https://login.microsoftonline.com"): + return + + user_id = update.effective_user.id + if user_id not in self._pending_auth: + return + + client = self._get_onedrive_client() + if not client: + return + + user_message = update.message # 保存用户消息引用 + pending = self._pending_auth[user_id] + flow = pending["state"] + auth_message = pending.get("message") # 认证指引消息 + + if await client.authenticate_with_code(text, flow=flow): + del self._pending_auth[user_id] + reply_message = await self._reply(update, context, "✅ OneDrive 认证成功!") + logger.info(f"OneDrive 认证成功 - {_get_user_info(update)}") + else: + # 认证失败时清理认证信息 + del self._pending_auth[user_id] + await client.logout() # 删除可能存在的旧 token + reply_message = await self._reply(update, context, "❌ 认证失败,请重试") + logger.error(f"OneDrive 认证失败 - {_get_user_info(update)}") + + # 延迟 5 秒后删除敏感消息(包括认证指引消息) + messages_to_delete = [msg for msg in [user_message, reply_message, auth_message] if msg] + if messages_to_delete: + asyncio.create_task(self._delayed_delete_messages(messages_to_delete)) + + async def cloud_logout(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """登出云存储""" + logger.info(f"收到云存储登出请求 - {_get_user_info(update)}") + client = self._get_onedrive_client() + if not client: + await self._reply(update, context, "❌ OneDrive 未配置") + return + + if await client.logout(): + await self._reply(update, context, "✅ 已登出 OneDrive") + else: + await self._reply(update, context, "❌ 登出失败") + + async def cloud_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """查看云存储状态""" + logger.info(f"收到云存储状态查询 - {_get_user_info(update)}") + client = self._get_onedrive_client() + if not client: + await self._reply(update, context, "❌ OneDrive 未配置") + return + + is_auth = await client.is_authenticated() + auto_upload = self._onedrive_config.auto_upload if self._onedrive_config else False + delete_after = self._onedrive_config.delete_after_upload if self._onedrive_config else False + remote_path = self._onedrive_config.remote_path if self._onedrive_config else "/aria2bot" + + text = ( + "☁️ *OneDrive 状态*\n\n" + f"🔐 认证状态: {'✅ 已认证' if is_auth else '❌ 未认证'}\n" + f"📤 自动上传: {'✅ 开启' if auto_upload else '❌ 关闭'}\n" + f"🗑️ 上传后删除: {'✅ 开启' if delete_after else '❌ 关闭'}\n" + f"📁 远程路径: `{remote_path}`" + ) + await self._reply(update, context, text, parse_mode="Markdown") + + async def upload_to_cloud(self, update: Update, context: ContextTypes.DEFAULT_TYPE, gid: str) -> None: + """上传文件到云存储""" + from pathlib import Path + import shutil + + logger.info(f"收到上传请求 GID={gid} - {_get_user_info(update)}") + client = self._get_onedrive_client() + if not client or not await client.is_authenticated(): + await self._reply(update, context, "❌ OneDrive 未认证,请先使用 /cloud 进行认证") + return + + rpc = self._get_rpc_client() + try: + task = await rpc.get_status(gid) + except RpcError as e: + await self._reply(update, context, f"❌ 获取任务信息失败: {e}") + return + + if task.status != "complete": + await self._reply(update, context, "❌ 任务未完成,无法上传") + return + + local_path = Path(task.dir) / task.name + if not local_path.exists(): + await self._reply(update, context, "❌ 本地文件不存在") + return + + # 计算远程路径(保持目录结构) + try: + download_dir = DOWNLOAD_DIR.resolve() + relative_path = local_path.resolve().relative_to(download_dir) + remote_path = f"{self._onedrive_config.remote_path}/{relative_path.parent}" + except ValueError: + remote_path = self._onedrive_config.remote_path + + msg = await self._reply(update, context, f"☁️ 正在上传: {task.name}\n⏳ 请稍候...") + + success = await client.upload_file(local_path, remote_path) + + if success: + result_text = f"✅ 上传成功: {task.name}" + if self._onedrive_config and self._onedrive_config.delete_after_upload: + try: + if local_path.is_dir(): + shutil.rmtree(local_path) + else: + local_path.unlink() + result_text += "\n🗑️ 本地文件已删除" + except Exception as e: + result_text += f"\n⚠️ 删除本地文件失败: {e}" + await msg.edit_text(result_text) + logger.info(f"上传成功 GID={gid} - {_get_user_info(update)}") + else: + await msg.edit_text(f"❌ 上传失败: {task.name}") + logger.error(f"上传失败 GID={gid} - {_get_user_info(update)}") + async def handle_button_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """处理 Reply Keyboard 按钮点击""" text = update.message.text @@ -559,6 +756,11 @@ class Aria2BotAPI: await self._handle_stats_callback(query, rpc) elif action == "cancel": await query.edit_message_text("❌ 操作已取消") + # 云存储相关回调 + elif action == "cloud": + await self._handle_cloud_callback(query, update, context, parts) + elif action == "upload": + await self._handle_upload_callback(query, update, context, parts) except RpcError as e: await query.edit_message_text(f"❌ 操作失败: {e}") @@ -740,7 +942,13 @@ class Aria2BotAPI: if task.error_message: text += f"\n❌ 错误: {task.error_message}" - keyboard = build_detail_keyboard(gid, task.status) + # 检查是否显示上传按钮(任务完成且云存储已配置) + show_upload = ( + task.status == "complete" and + self._onedrive_config and + self._onedrive_config.enabled + ) + keyboard = build_detail_keyboard_with_upload(gid, task.status, show_upload) # 只有内容变化时才更新 if text != last_text: @@ -774,6 +982,83 @@ class Aria2BotAPI: keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 返回列表", callback_data="list:menu")]]) await query.edit_message_text(text, parse_mode="Markdown", reply_markup=keyboard) + # === 云存储回调处理 === + + async def _handle_cloud_callback(self, query, update: Update, context: ContextTypes.DEFAULT_TYPE, parts: list) -> None: + """处理云存储相关回调""" + if len(parts) < 2: + await query.edit_message_text("❌ 无效操作") + return + + sub_action = parts[1] + + if sub_action == "auth": + # 认证请求 + await self.cloud_auth(update, context) + elif sub_action == "status": + # 状态查询 + client = self._get_onedrive_client() + if not client: + await query.edit_message_text("❌ OneDrive 未配置") + return + is_auth = await client.is_authenticated() + auto_upload = self._onedrive_config.auto_upload if self._onedrive_config else False + delete_after = self._onedrive_config.delete_after_upload if self._onedrive_config else False + remote_path = self._onedrive_config.remote_path if self._onedrive_config else "/aria2bot" + text = ( + "☁️ *OneDrive 状态*\n\n" + f"🔐 认证状态: {'✅ 已认证' if is_auth else '❌ 未认证'}\n" + f"📤 自动上传: {'✅ 开启' if auto_upload else '❌ 关闭'}\n" + f"🗑️ 上传后删除: {'✅ 开启' if delete_after else '❌ 关闭'}\n" + f"📁 远程路径: `{remote_path}`" + ) + keyboard = build_cloud_menu_keyboard() + await query.edit_message_text(text, parse_mode="Markdown", reply_markup=keyboard) + elif sub_action == "settings": + # 设置页面 + auto_upload = self._onedrive_config.auto_upload if self._onedrive_config else False + delete_after = self._onedrive_config.delete_after_upload if self._onedrive_config else False + keyboard = build_cloud_settings_keyboard(auto_upload, delete_after) + await query.edit_message_text("⚙️ *云存储设置*\n\n点击切换设置:", parse_mode="Markdown", reply_markup=keyboard) + elif sub_action == "logout": + # 登出 + client = self._get_onedrive_client() + if client and await client.logout(): + await query.edit_message_text("✅ 已登出 OneDrive") + else: + await query.edit_message_text("❌ 登出失败") + elif sub_action == "menu": + # 返回菜单 + keyboard = build_cloud_menu_keyboard() + await query.edit_message_text("☁️ *云存储管理*", parse_mode="Markdown", reply_markup=keyboard) + elif sub_action == "toggle": + # 切换设置(注意:运行时修改配置,重启后会重置) + if len(parts) < 3: + return + setting = parts[2] + if self._onedrive_config: + if setting == "auto_upload": + self._onedrive_config.auto_upload = not self._onedrive_config.auto_upload + elif setting == "delete_after": + self._onedrive_config.delete_after_upload = not self._onedrive_config.delete_after_upload + auto_upload = self._onedrive_config.auto_upload if self._onedrive_config else False + delete_after = self._onedrive_config.delete_after_upload if self._onedrive_config else False + keyboard = build_cloud_settings_keyboard(auto_upload, delete_after) + await query.edit_message_text("⚙️ *云存储设置*\n\n点击切换设置:", parse_mode="Markdown", reply_markup=keyboard) + + async def _handle_upload_callback(self, query, update: Update, context: ContextTypes.DEFAULT_TYPE, parts: list) -> None: + """处理上传回调""" + if len(parts) < 3: + await query.edit_message_text("❌ 无效操作") + return + + provider = parts[1] # onedrive + gid = parts[2] + + if provider == "onedrive": + await query.edit_message_text("☁️ 正在准备上传...") + await self.upload_to_cloud(update, context, gid) + def build_handlers(api: Aria2BotAPI) -> list: """构建 Handler 列表""" @@ -808,8 +1093,12 @@ def build_handlers(api: Aria2BotAPI) -> list: CommandHandler("add", wrap_with_permission(api.add_download)), CommandHandler("list", wrap_with_permission(api.list_downloads)), CommandHandler("stats", wrap_with_permission(api.global_stats)), + # 云存储命令 + CommandHandler("cloud", wrap_with_permission(api.cloud_command)), # Reply Keyboard 按钮文本处理 MessageHandler(filters.TEXT & filters.Regex(button_pattern), wrap_with_permission(api.handle_button_text)), + # OneDrive 认证回调 URL 处理 + MessageHandler(filters.TEXT & filters.Regex(r"^https://login\.microsoftonline\.com"), wrap_with_permission(api.handle_auth_callback)), # 种子文件处理 MessageHandler(filters.Document.FileExtension("torrent"), wrap_with_permission(api.handle_torrent)), # Callback Query 处理 diff --git a/src/telegram/keyboards.py b/src/telegram/keyboards.py index 9d81192..fd80189 100644 --- a/src/telegram/keyboards.py +++ b/src/telegram/keyboards.py @@ -115,3 +115,62 @@ def build_main_reply_keyboard() -> ReplyKeyboardMarkup: [KeyboardButton("📜 日志"), KeyboardButton("❓ 帮助")], ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, is_persistent=True) + + +# ==================== 云存储相关键盘 ==================== + + +def build_cloud_menu_keyboard() -> InlineKeyboardMarkup: + """构建云存储管理菜单""" + return InlineKeyboardMarkup([ + [InlineKeyboardButton("🔐 OneDrive 认证", callback_data="cloud:auth:onedrive")], + [ + InlineKeyboardButton("📊 状态", callback_data="cloud:status"), + InlineKeyboardButton("⚙️ 设置", callback_data="cloud:settings"), + ], + [InlineKeyboardButton("🚪 登出", callback_data="cloud:logout")], + ]) + + +def build_upload_choice_keyboard(gid: str) -> InlineKeyboardMarkup: + """构建下载完成后的上传选择键盘""" + return InlineKeyboardMarkup([ + [InlineKeyboardButton("☁️ 上传到 OneDrive", callback_data=f"upload:onedrive:{gid}")], + [InlineKeyboardButton("🔙 返回列表", callback_data="list:menu")], + ]) + + +def build_cloud_settings_keyboard(auto_upload: bool, delete_after: bool) -> InlineKeyboardMarkup: + """构建云存储设置键盘""" + auto_text = "✅ 自动上传" if auto_upload else "❌ 自动上传" + delete_text = "✅ 上传后删除" if delete_after else "❌ 上传后删除" + return InlineKeyboardMarkup([ + [InlineKeyboardButton(auto_text, callback_data="cloud:toggle:auto_upload")], + [InlineKeyboardButton(delete_text, callback_data="cloud:toggle:delete_after")], + [InlineKeyboardButton("🔙 返回", callback_data="cloud:menu")], + ]) + + +def build_detail_keyboard_with_upload(gid: str, status: str, show_upload: bool = False) -> InlineKeyboardMarkup: + """构建详情页面的操作按钮(含上传选项)""" + buttons = [] + + if status == "active": + buttons.append(InlineKeyboardButton("⏸ 暂停", callback_data=f"pause:{gid}")) + elif status in ("paused", "waiting"): + buttons.append(InlineKeyboardButton("▶️ 恢复", callback_data=f"resume:{gid}")) + + buttons.append(InlineKeyboardButton("🗑 删除", callback_data=f"delete:{gid}")) + + rows = [buttons] + + # 任务完成时显示上传按钮 + if show_upload and status == "complete": + rows.append([InlineKeyboardButton("☁️ 上传到云盘", callback_data=f"upload:onedrive:{gid}")]) + + rows.append([ + InlineKeyboardButton("🔄 刷新", callback_data=f"refresh:{gid}"), + InlineKeyboardButton("🔙 返回列表", callback_data="list:menu"), + ]) + + return InlineKeyboardMarkup(rows) diff --git a/uv.lock b/uv.lock index e198cdd..7e34663 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "httpx" }, + { name = "o365" }, { name = "python-dotenv" }, { name = "python-telegram-bot" }, ] @@ -27,10 +28,24 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, + { name = "o365", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-telegram-bot", specifier = ">=21.0" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -40,6 +55,148 @@ wheels = [ { 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" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -86,6 +243,72 @@ wheels = [ { 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" }, ] +[[package]] +name = "msal" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, +] + +[[package]] +name = "o365" +version = "2.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "msal" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "tzdata" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/56/db77ef456e1240db67e8f763e8ca454b490974fe66339d43cb53ad43fcef/o365-2.1.8.tar.gz", hash = "sha256:3fcb371e82cffa7b4fee758e5f64fe97e18abc6c92a45c236688cbd15b4bdd45", size = 144253, upload-time = "2025-11-28T10:46:33.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/4a/8d81e878d7b81f396852355b5726cdcd0f8a5b4e5f8c6a1a67f94734be0e/o365-2.1.8-py3-none-any.whl", hash = "sha256:7527ed05bce04475eef35608e48733f569c908e93b6b49815c42f6d00b1adf61", size = 159776, upload-time = "2025-11-28T10:46:30.909Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +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" } +wheels = [ + { 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" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -106,3 +329,75 @@ sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1 wheels = [ { 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" }, ] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +]