mirror of
https://github.com/dnslin/aria2bot.git
synced 2026-01-11 04:02:20 +08:00
feat: 增加OneDrive认证功能
This commit is contained in:
22
.env.example
22
.env.example
@@ -12,3 +12,25 @@ ARIA2_RPC_PORT=6800
|
|||||||
|
|
||||||
# Aria2 RPC Secret (optional, auto-generated if empty)
|
# Aria2 RPC Secret (optional, auto-generated if empty)
|
||||||
ARIA2_RPC_SECRET=
|
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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ dependencies = [
|
|||||||
"python-dotenv>=1.2.1",
|
"python-dotenv>=1.2.1",
|
||||||
"python-telegram-bot>=21.0",
|
"python-telegram-bot>=21.0",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
|
"O365>=2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
11
src/cloud/__init__.py
Normal file
11
src/cloud/__init__.py
Normal file
@@ -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
|
||||||
91
src/cloud/base.py
Normal file
91
src/cloud/base.py
Normal file
@@ -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
|
||||||
230
src/cloud/onedrive.py
Normal file
230
src/cloud/onedrive.py
Normal file
@@ -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
|
||||||
@@ -18,12 +18,25 @@ class Aria2Config:
|
|||||||
bt_tracker_update: bool = True
|
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
|
@dataclass
|
||||||
class BotConfig:
|
class BotConfig:
|
||||||
token: str = ""
|
token: str = ""
|
||||||
api_base_url: str = ""
|
api_base_url: str = ""
|
||||||
allowed_users: set[int] = field(default_factory=set)
|
allowed_users: set[int] = field(default_factory=set)
|
||||||
aria2: Aria2Config = field(default_factory=Aria2Config)
|
aria2: Aria2Config = field(default_factory=Aria2Config)
|
||||||
|
onedrive: OneDriveConfig = field(default_factory=OneDriveConfig)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "BotConfig":
|
def from_env(cls) -> "BotConfig":
|
||||||
@@ -62,9 +75,22 @@ class BotConfig:
|
|||||||
rpc_port=rpc_port,
|
rpc_port=rpc_port,
|
||||||
rpc_secret=os.environ.get("ARIA2_RPC_SECRET", ""),
|
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(
|
return cls(
|
||||||
token=token,
|
token=token,
|
||||||
api_base_url=os.environ.get("TELEGRAM_API_BASE_URL", ""),
|
api_base_url=os.environ.get("TELEGRAM_API_BASE_URL", ""),
|
||||||
allowed_users=allowed_users,
|
allowed_users=allowed_users,
|
||||||
aria2=aria2,
|
aria2=aria2,
|
||||||
|
onedrive=onedrive,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ ARIA2_DHT6 = ARIA2_CONFIG_DIR / "dht6.dat"
|
|||||||
DOWNLOAD_DIR = HOME / "downloads"
|
DOWNLOAD_DIR = HOME / "downloads"
|
||||||
SYSTEMD_USER_DIR = HOME / ".config" / "systemd" / "user"
|
SYSTEMD_USER_DIR = HOME / ".config" / "systemd" / "user"
|
||||||
ARIA2_SERVICE = SYSTEMD_USER_DIR / "aria2.service"
|
ARIA2_SERVICE = SYSTEMD_USER_DIR / "aria2.service"
|
||||||
|
|
||||||
|
# 云存储相关路径
|
||||||
|
CLOUD_TOKEN_DIR = ARIA2_CONFIG_DIR / "cloud_tokens"
|
||||||
|
|||||||
@@ -31,3 +31,15 @@ class NotInstalledError(Aria2Error):
|
|||||||
|
|
||||||
class RpcError(Aria2Error):
|
class RpcError(Aria2Error):
|
||||||
"""RPC 调用失败"""
|
"""RPC 调用失败"""
|
||||||
|
|
||||||
|
|
||||||
|
class CloudStorageError(Aria2Error):
|
||||||
|
"""云存储错误基类"""
|
||||||
|
|
||||||
|
|
||||||
|
class CloudAuthError(CloudStorageError):
|
||||||
|
"""云存储认证错误"""
|
||||||
|
|
||||||
|
|
||||||
|
class CloudUploadError(CloudStorageError):
|
||||||
|
"""云存储上传错误"""
|
||||||
|
|||||||
@@ -28,11 +28,14 @@ BOT_COMMANDS = [
|
|||||||
BotCommand("add", "添加下载任务"),
|
BotCommand("add", "添加下载任务"),
|
||||||
BotCommand("list", "查看下载列表"),
|
BotCommand("list", "查看下载列表"),
|
||||||
BotCommand("stats", "全局下载统计"),
|
BotCommand("stats", "全局下载统计"),
|
||||||
|
BotCommand("cloud", "云存储管理"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def post_init(application: Application) -> None:
|
async def post_init(application: Application) -> None:
|
||||||
"""应用初始化后设置命令菜单"""
|
"""应用初始化后设置命令菜单"""
|
||||||
|
logger = setup_logger()
|
||||||
|
logger.info("Setting bot commands...")
|
||||||
await application.bot.set_my_commands(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")
|
builder = builder.base_url(config.api_base_url).base_file_url(config.api_base_url + "/file")
|
||||||
app = builder.build()
|
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):
|
for handler in build_handlers(api):
|
||||||
app.add_handler(handler)
|
app.add_handler(handler)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ from src.core import (
|
|||||||
get_aria2_version,
|
get_aria2_version,
|
||||||
generate_rpc_secret,
|
generate_rpc_secret,
|
||||||
ARIA2_CONF,
|
ARIA2_CONF,
|
||||||
|
DOWNLOAD_DIR,
|
||||||
)
|
)
|
||||||
|
from src.core.config import OneDriveConfig
|
||||||
from src.aria2 import Aria2Installer, Aria2ServiceManager
|
from src.aria2 import Aria2Installer, Aria2ServiceManager
|
||||||
from src.aria2.rpc import Aria2RpcClient, DownloadTask, _format_size
|
from src.aria2.rpc import Aria2RpcClient, DownloadTask, _format_size
|
||||||
from src.telegram.keyboards import (
|
from src.telegram.keyboards import (
|
||||||
@@ -32,6 +34,10 @@ from src.telegram.keyboards import (
|
|||||||
build_detail_keyboard,
|
build_detail_keyboard,
|
||||||
build_after_add_keyboard,
|
build_after_add_keyboard,
|
||||||
build_main_reply_keyboard,
|
build_main_reply_keyboard,
|
||||||
|
build_cloud_menu_keyboard,
|
||||||
|
build_upload_choice_keyboard,
|
||||||
|
build_cloud_settings_keyboard,
|
||||||
|
build_detail_keyboard_with_upload,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reply Keyboard 按钮文本到命令的映射
|
# Reply Keyboard 按钮文本到命令的映射
|
||||||
@@ -83,13 +89,18 @@ import asyncio
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
class Aria2BotAPI:
|
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.config = config or Aria2Config()
|
||||||
self.allowed_users = allowed_users or set()
|
self.allowed_users = allowed_users or set()
|
||||||
self.installer = Aria2Installer(self.config)
|
self.installer = Aria2Installer(self.config)
|
||||||
self.service = Aria2ServiceManager()
|
self.service = Aria2ServiceManager()
|
||||||
self._rpc: Aria2RpcClient | None = None
|
self._rpc: Aria2RpcClient | None = None
|
||||||
self._auto_refresh_tasks: dict[str, asyncio.Task] = {} # chat_id:msg_id -> task
|
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:
|
async def _check_permission(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
||||||
"""检查用户权限,返回 True 表示有权限"""
|
"""检查用户权限,返回 True 表示有权限"""
|
||||||
@@ -113,6 +124,13 @@ class Aria2BotAPI:
|
|||||||
self._rpc = Aria2RpcClient(port=port, secret=secret)
|
self._rpc = Aria2RpcClient(port=port, secret=secret)
|
||||||
return self._rpc
|
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):
|
async def _reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE, text: str, **kwargs):
|
||||||
if update.effective_message:
|
if update.effective_message:
|
||||||
return await update.effective_message.reply_text(text, **kwargs)
|
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 await context.bot.send_message(chat_id=update.effective_chat.id, text=text, **kwargs)
|
||||||
return None
|
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:
|
def _get_rpc_secret(self) -> str:
|
||||||
if self.config.rpc_secret:
|
if self.config.rpc_secret:
|
||||||
return self.config.rpc_secret
|
return self.config.rpc_secret
|
||||||
@@ -378,6 +409,9 @@ class Aria2BotAPI:
|
|||||||
"/list - 查看下载列表",
|
"/list - 查看下载列表",
|
||||||
"/stats - 全局下载统计",
|
"/stats - 全局下载统计",
|
||||||
"",
|
"",
|
||||||
|
"*云存储*",
|
||||||
|
"/cloud - 云存储管理菜单",
|
||||||
|
"",
|
||||||
"/menu - 显示快捷菜单",
|
"/menu - 显示快捷菜单",
|
||||||
"/help - 显示此帮助",
|
"/help - 显示此帮助",
|
||||||
]
|
]
|
||||||
@@ -394,6 +428,169 @@ class Aria2BotAPI:
|
|||||||
reply_markup=keyboard
|
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:
|
async def handle_button_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""处理 Reply Keyboard 按钮点击"""
|
"""处理 Reply Keyboard 按钮点击"""
|
||||||
text = update.message.text
|
text = update.message.text
|
||||||
@@ -559,6 +756,11 @@ class Aria2BotAPI:
|
|||||||
await self._handle_stats_callback(query, rpc)
|
await self._handle_stats_callback(query, rpc)
|
||||||
elif action == "cancel":
|
elif action == "cancel":
|
||||||
await query.edit_message_text("❌ 操作已取消")
|
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:
|
except RpcError as e:
|
||||||
await query.edit_message_text(f"❌ 操作失败: {e}")
|
await query.edit_message_text(f"❌ 操作失败: {e}")
|
||||||
@@ -740,7 +942,13 @@ class Aria2BotAPI:
|
|||||||
if task.error_message:
|
if task.error_message:
|
||||||
text += f"\n❌ 错误: {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:
|
if text != last_text:
|
||||||
@@ -774,6 +982,83 @@ class Aria2BotAPI:
|
|||||||
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 返回列表", callback_data="list:menu")]])
|
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 返回列表", callback_data="list:menu")]])
|
||||||
await query.edit_message_text(text, parse_mode="Markdown", reply_markup=keyboard)
|
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:
|
def build_handlers(api: Aria2BotAPI) -> list:
|
||||||
"""构建 Handler 列表"""
|
"""构建 Handler 列表"""
|
||||||
@@ -808,8 +1093,12 @@ def build_handlers(api: Aria2BotAPI) -> list:
|
|||||||
CommandHandler("add", wrap_with_permission(api.add_download)),
|
CommandHandler("add", wrap_with_permission(api.add_download)),
|
||||||
CommandHandler("list", wrap_with_permission(api.list_downloads)),
|
CommandHandler("list", wrap_with_permission(api.list_downloads)),
|
||||||
CommandHandler("stats", wrap_with_permission(api.global_stats)),
|
CommandHandler("stats", wrap_with_permission(api.global_stats)),
|
||||||
|
# 云存储命令
|
||||||
|
CommandHandler("cloud", wrap_with_permission(api.cloud_command)),
|
||||||
# Reply Keyboard 按钮文本处理
|
# Reply Keyboard 按钮文本处理
|
||||||
MessageHandler(filters.TEXT & filters.Regex(button_pattern), wrap_with_permission(api.handle_button_text)),
|
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)),
|
MessageHandler(filters.Document.FileExtension("torrent"), wrap_with_permission(api.handle_torrent)),
|
||||||
# Callback Query 处理
|
# Callback Query 处理
|
||||||
|
|||||||
@@ -115,3 +115,62 @@ def build_main_reply_keyboard() -> ReplyKeyboardMarkup:
|
|||||||
[KeyboardButton("📜 日志"), KeyboardButton("❓ 帮助")],
|
[KeyboardButton("📜 日志"), KeyboardButton("❓ 帮助")],
|
||||||
]
|
]
|
||||||
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, is_persistent=True)
|
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)
|
||||||
|
|||||||
295
uv.lock
generated
295
uv.lock
generated
@@ -20,6 +20,7 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "o365" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "python-telegram-bot" },
|
{ name = "python-telegram-bot" },
|
||||||
]
|
]
|
||||||
@@ -27,10 +28,24 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
|
{ name = "o365", specifier = ">=2.0.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||||
{ name = "python-telegram-bot", specifier = ">=21.0" },
|
{ 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]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.11.12"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -106,3 +329,75 @@ sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1
|
|||||||
wheels = [
|
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" },
|
{ 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" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user