mirror of
https://github.com/nikdoof/smsbot.git
synced 2025-12-13 18:12:15 +00:00
Update to support python-telegram-bot v23
This commit is contained in:
@@ -15,3 +15,8 @@ tasks:
|
||||
desc: Build the container using Docker
|
||||
cmds:
|
||||
- docker build . -t smsbot:latest
|
||||
|
||||
smsbot:run:
|
||||
desc: Run the SMSBot
|
||||
cmds:
|
||||
- uv run smsbot
|
||||
@@ -1,17 +1,18 @@
|
||||
[project]
|
||||
name = "smsbot"
|
||||
version = "0.0.7"
|
||||
version = "0.1.0"
|
||||
description = "A simple Telegram bot to receive SMS messages."
|
||||
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9,<3.10"
|
||||
dependencies = [
|
||||
"flask>=3.1.1",
|
||||
"flask[async]>=3.1.1",
|
||||
"prometheus-async>=25.1.0",
|
||||
"prometheus-client>=0.22.1",
|
||||
"python-telegram-bot<20",
|
||||
"python-telegram-bot>=22.3",
|
||||
"twilio>=9.7.0",
|
||||
"waitress>=3.0.2",
|
||||
"uvicorn>=0.35.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
import uvicorn
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
|
||||
from smsbot.telegram import TelegramSmsBot
|
||||
from smsbot.utils import get_smsbot_version
|
||||
@@ -13,7 +18,7 @@ def main():
|
||||
"--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or "80"
|
||||
"--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or 80, type=int
|
||||
)
|
||||
parser.add_argument(
|
||||
"--telegram-bot-token", default=os.environ.get("SMSBOT_TELEGRAM_BOT_TOKEN")
|
||||
@@ -22,31 +27,74 @@ def main():
|
||||
parser.add_argument(
|
||||
"--default-subscribers", default=os.environ.get("SMSBOT_DEFAULT_SUBSCRIBERS")
|
||||
)
|
||||
parser.add_argument("--log-level", default="INFO")
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# TODO: Replace for Py >=3.11
|
||||
logging.basicConfig(level=logging.getLevelName(args.log_level))
|
||||
if args.debug:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
logging.basicConfig(level=level)
|
||||
logging.info("smsbot v%s", get_smsbot_version())
|
||||
logging.debug("Arguments: %s", args)
|
||||
|
||||
# Start bot
|
||||
telegram_bot = TelegramSmsBot(args.telegram_bot_token)
|
||||
telegram_bot = TelegramSmsBot(token=args.telegram_bot_token)
|
||||
|
||||
# Set the owner ID if configured
|
||||
if args.owner_id:
|
||||
telegram_bot.set_owner(args.owner_id)
|
||||
telegram_bot.owners = [int(args.owner_id)]
|
||||
else:
|
||||
logging.warning("No Owner ID is set, which is not a good idea...")
|
||||
|
||||
# Add default subscribers
|
||||
if args.default_subscribers:
|
||||
for chat_id in args.default_subscribers.split(","):
|
||||
telegram_bot.add_subscriber(chat_id)
|
||||
telegram_bot.subscribers.append(int(chat_id.strip()))
|
||||
|
||||
telegram_bot.start()
|
||||
|
||||
# Start webhooks
|
||||
webhooks = TwilioWebhookHandler()
|
||||
webhooks.set_bot(telegram_bot)
|
||||
webhooks.serve(host=args.listen_host, port=args.listen_port)
|
||||
|
||||
# Build a uvicorn ASGI server
|
||||
flask_app = uvicorn.Server(
|
||||
config=uvicorn.Config(
|
||||
app=WsgiToAsgi(webhooks.app),
|
||||
port=args.listen_port,
|
||||
use_colors=False,
|
||||
host=args.listen_host,
|
||||
)
|
||||
)
|
||||
|
||||
# Loop until exit
|
||||
loop = asyncio.get_event_loop()
|
||||
main_task = asyncio.ensure_future(run_bot(telegram_bot, flask_app))
|
||||
for signal in [SIGINT, SIGTERM]:
|
||||
loop.add_signal_handler(signal, main_task.cancel)
|
||||
try:
|
||||
loop.run_until_complete(main_task)
|
||||
# Catch graceful shutdowns
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def run_bot(telegram_bot: TelegramSmsBot, flask_app):
|
||||
# Start async Telegram bot
|
||||
try:
|
||||
# Start the bot
|
||||
await telegram_bot.app.initialize()
|
||||
await telegram_bot.app.start()
|
||||
await telegram_bot.app.updater.start_polling()
|
||||
|
||||
# Startup uvicorn/flask
|
||||
await flask_app.serve()
|
||||
|
||||
# Run the bot idle loop
|
||||
await telegram_bot.app.updater.idle()
|
||||
finally:
|
||||
# Shutdown in reverse order
|
||||
await flask_app.shutdown()
|
||||
await telegram_bot.app.updater.stop()
|
||||
await telegram_bot.app.stop()
|
||||
await telegram_bot.app.shutdown()
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import logging
|
||||
|
||||
from prometheus_client import Counter, Summary
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
||||
from smsbot.utils import get_smsbot_version
|
||||
from prometheus_client import Counter, Summary
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
ApplicationHandlerStop,
|
||||
ContextTypes,
|
||||
TypeHandler,
|
||||
CommandHandler,
|
||||
)
|
||||
|
||||
REQUEST_TIME = Summary(
|
||||
"telegram_request_processing_seconds", "Time spent processing request"
|
||||
@@ -11,108 +17,94 @@ REQUEST_TIME = Summary(
|
||||
COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands processed")
|
||||
|
||||
|
||||
class TelegramSmsBot(object):
|
||||
def __init__(
|
||||
self, telegram_token, allow_subscribing=False, owner=None, subscribers=None
|
||||
):
|
||||
class TelegramSmsBot:
|
||||
def __init__(self, token: str, owners: list[int] = [], subscribers: list[int] = []):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.bot_token = telegram_token
|
||||
self.subscriber_ids = subscribers or []
|
||||
self.set_owner(owner)
|
||||
self.app = Application.builder().token(token).build()
|
||||
self.owners = owners
|
||||
self.subscribers = subscribers
|
||||
|
||||
self.updater = Updater(self.bot_token, use_context=True)
|
||||
self.updater.dispatcher.add_handler(CommandHandler("help", self.help_handler))
|
||||
self.updater.dispatcher.add_handler(CommandHandler("start", self.help_handler))
|
||||
self.init_handlers()
|
||||
|
||||
if allow_subscribing:
|
||||
self.updater.dispatcher.add_handler(
|
||||
CommandHandler("subscribe", self.subscribe_handler)
|
||||
def init_handlers(self):
|
||||
self.app.add_handler(TypeHandler(Update, self.callback), -1)
|
||||
self.app.add_handler(CommandHandler("help", self.handler_help))
|
||||
self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe))
|
||||
self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe))
|
||||
|
||||
async def callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle the update"""
|
||||
if update.effective_user.id in self.owners:
|
||||
self.logger.info(
|
||||
f"{update.effective_user.username} sent {update.message.text}"
|
||||
)
|
||||
self.updater.dispatcher.add_handler(
|
||||
CommandHandler("unsubscribe", self.unsubscribe_handler)
|
||||
COMMAND_COUNT.inc()
|
||||
else:
|
||||
self.logger.debug(f"Ignoring message from user {update.effective_user.id}")
|
||||
raise ApplicationHandlerStop
|
||||
|
||||
async def send_message(self, chat_id: int, text: str):
|
||||
"""Send a message to a specific chat"""
|
||||
self.logger.info(f"Sending message to chat {chat_id}: {text}")
|
||||
await self.app.bot.send_message(
|
||||
chat_id=chat_id, text=text, parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
self.updater.dispatcher.add_error_handler(self.error_handler)
|
||||
async def send_subscribers(self, text: str):
|
||||
"""Send a message to all subscribers"""
|
||||
for subscriber in self.subscribers:
|
||||
self.logger.info(f"Sending message to subscriber {subscriber}")
|
||||
await self.send_message(subscriber, text)
|
||||
|
||||
def start(self):
|
||||
self.logger.info("Starting bot...")
|
||||
self.updater.start_polling()
|
||||
self.bot = self.updater.bot
|
||||
self.logger.info("Bot Ready")
|
||||
|
||||
def stop(self):
|
||||
self.updater.stop()
|
||||
async def send_owners(self, text: str):
|
||||
"""Send a message to all owners"""
|
||||
for owner in self.owners:
|
||||
self.logger.info(f"Sending message to owner {owner}")
|
||||
await self.send_message(owner, text)
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
def help_handler(self, update, context):
|
||||
async def handler_help(self, update, context):
|
||||
"""Send a message when the command /help is issued."""
|
||||
self.logger.info("/help command received in chat: %s", update.message.chat)
|
||||
|
||||
commands = []
|
||||
for command in self.updater.dispatcher.handlers[0]:
|
||||
commands.extend(["/{0}".format(cmd) for cmd in command.command])
|
||||
for command in self.app.handlers[0]:
|
||||
if isinstance(command, CommandHandler):
|
||||
commands.extend(["/{0}".format(cmd) for cmd in command.commands])
|
||||
|
||||
update.message.reply_markdown(
|
||||
await update.message.reply_markdown(
|
||||
"Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands))
|
||||
)
|
||||
COMMAND_COUNT.inc()
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
def subscribe_handler(self, update, context):
|
||||
self.logger.info("/subscribe command received")
|
||||
if update.message.chat["id"] not in self.subscriber_ids:
|
||||
self.logger.info("{0} subscribed".format(update.message.chat["username"]))
|
||||
self.subscriber_ids.append(update.message.chat["id"])
|
||||
self.send_owner(
|
||||
"{0} has subscribed".format(update.message.chat["username"])
|
||||
)
|
||||
update.message.reply_markdown(
|
||||
"You have been subscribed to SMS notifications"
|
||||
async def handler_subscribe(
|
||||
self, update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
):
|
||||
"""Handle subscription requests"""
|
||||
user_id = update.effective_user.id
|
||||
if user_id not in self.subscribers:
|
||||
self.subscribers.append(user_id)
|
||||
self.logger.info(f"User {user_id} subscribed.")
|
||||
self.logger.info(f"Current subscribers: {self.subscribers}")
|
||||
await update.message.reply_markdown(
|
||||
"You have successfully subscribed to updates."
|
||||
)
|
||||
else:
|
||||
update.message.reply_markdown(
|
||||
"You are already subscribed to SMS notifications"
|
||||
)
|
||||
COMMAND_COUNT.inc()
|
||||
self.logger.info(f"User {user_id} is already subscribed.")
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
def unsubscribe_handler(self, update, context):
|
||||
self.logger.info("/unsubscribe command received")
|
||||
if update.message.chat["id"] in self.subscriber_ids:
|
||||
self.logger.info("{0} unsubscribed".format(update.message.chat["username"]))
|
||||
self.subscriber_ids.remove(update.message.chat["id"])
|
||||
self.send_owner(
|
||||
"{0} has unsubscribed".format(update.message.chat["username"])
|
||||
)
|
||||
update.message.reply_markdown(
|
||||
"You have been unsubscribed to SMS notifications"
|
||||
async def handler_unsubscribe(
|
||||
self, update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
):
|
||||
"""Handle unsubscription requests"""
|
||||
user_id = update.effective_user.id
|
||||
if user_id in self.subscribers:
|
||||
self.subscribers.remove(user_id)
|
||||
self.logger.info(f"User {user_id} unsubscribed.")
|
||||
self.logger.info(f"Current subscribers: {self.subscribers}")
|
||||
await update.message.reply_markdown(
|
||||
"You have successfully unsubscribed from updates."
|
||||
)
|
||||
else:
|
||||
update.message.reply_markdown("You are not subscribed to SMS notifications")
|
||||
COMMAND_COUNT.inc()
|
||||
|
||||
def error_handler(self, update, context):
|
||||
"""Log Errors caused by Updates."""
|
||||
self.logger.warning('Update "%s" caused error "%s"', update, context.error)
|
||||
self.send_owner(
|
||||
'Update "%{0}" caused error "{1}"'.format(update, context.error)
|
||||
)
|
||||
|
||||
def send_message(self, message, chat_id):
|
||||
self.bot.sendMessage(text=message, chat_id=chat_id)
|
||||
|
||||
def send_owner(self, message):
|
||||
if self.owner_id:
|
||||
self.send_message(message, self.owner_id)
|
||||
|
||||
def send_subscribers(self, message):
|
||||
for chat_id in self.subscriber_ids:
|
||||
self.send_message(message, chat_id)
|
||||
|
||||
def set_owner(self, chat_id):
|
||||
self.owner_id = chat_id
|
||||
if self.owner_id and self.owner_id not in self.subscriber_ids:
|
||||
self.subscriber_ids.append(self.owner_id)
|
||||
|
||||
def add_subscriber(self, chat_id):
|
||||
self.subscriber_ids.append(chat_id)
|
||||
self.logger.info(f"User {user_id} is not subscribed.")
|
||||
|
||||
@@ -3,3 +3,57 @@ from importlib.metadata import version
|
||||
|
||||
def get_smsbot_version():
|
||||
return version("smsbot")
|
||||
|
||||
|
||||
class TwilioMessage:
|
||||
"""
|
||||
Parses a Twilio webhook message.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.from_number: str = data.get("From", "Unknown")
|
||||
self.to_number: str = data.get("To", "Unknown")
|
||||
self.body: str = data.get("Body", "")
|
||||
|
||||
self.media = []
|
||||
for i in range(0, int(data.get("NumMedia", "0"))):
|
||||
self.media.append(data.get(f"MediaUrl{i}"))
|
||||
|
||||
def _escape(self, text: str) -> str:
|
||||
"""Escape text for MarkdownV2"""
|
||||
characters = [
|
||||
"_",
|
||||
"*",
|
||||
"[",
|
||||
"]",
|
||||
"(",
|
||||
")",
|
||||
"~",
|
||||
"`",
|
||||
">",
|
||||
"#",
|
||||
"+",
|
||||
"-",
|
||||
"=",
|
||||
"|",
|
||||
"{",
|
||||
"}",
|
||||
".",
|
||||
"!",
|
||||
]
|
||||
for char in characters:
|
||||
text = text.replace(char, rf"\{char}")
|
||||
return text
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TwilioWebhookMessage(from={self.from_number}, to={self.to_number})"
|
||||
|
||||
def to_str(self) -> str:
|
||||
media_str = "\n".join([f"<{url}>" for url in self.media]) if self.media else ""
|
||||
msg = f"**From**: {self.from_number}\n**To**: {self.to_number}\n\n{self.body}\n\n{media_str}"
|
||||
return msg
|
||||
|
||||
def to_markdownv2(self):
|
||||
media_str = "\n".join([f"{self._escape(url)}" for url in self.media]) if self.media else ""
|
||||
msg = f"**From**: {self._escape(self.from_number)}\n**To**: {self._escape(self.to_number)}\n\n{self._escape(self.body)}\n\n{media_str}"
|
||||
return msg
|
||||
|
||||
@@ -2,14 +2,16 @@ import os
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, abort, current_app, request
|
||||
from prometheus_async.aio import time
|
||||
from prometheus_client import Counter, Summary, make_wsgi_app
|
||||
from twilio.request_validator import RequestValidator
|
||||
from waitress import serve
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
from smsbot.utils import get_smsbot_version
|
||||
from smsbot.utils import TwilioMessage, get_smsbot_version
|
||||
|
||||
REQUEST_TIME = Summary("webhook_request_processing_seconds", "Time spent processing request")
|
||||
REQUEST_TIME = Summary(
|
||||
"webhook_request_processing_seconds", "Time spent processing request"
|
||||
)
|
||||
MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed")
|
||||
CALL_COUNT = Counter("webhook_call_count", "Total number of calls processed")
|
||||
|
||||
@@ -18,7 +20,7 @@ def validate_twilio_request(func):
|
||||
"""Validates that incoming requests genuinely originated from Twilio"""
|
||||
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwargs): # noqa: WPS430
|
||||
async def decorated_function(*args, **kwargs):
|
||||
# Create an instance of the RequestValidator class
|
||||
twilio_token = os.environ.get("SMSBOT_TWILIO_AUTH_TOKEN")
|
||||
|
||||
@@ -26,7 +28,7 @@ def validate_twilio_request(func):
|
||||
current_app.logger.warning(
|
||||
"Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing"
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
validator = RequestValidator(twilio_token)
|
||||
|
||||
@@ -63,47 +65,47 @@ class TwilioWebhookHandler(object):
|
||||
},
|
||||
)
|
||||
|
||||
def set_bot(self, bot): # noqa: WPS615
|
||||
def set_bot(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
def index(self):
|
||||
return ""
|
||||
async def index(self):
|
||||
return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>'
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
def health(self):
|
||||
async def health(self):
|
||||
"""Return basic health information"""
|
||||
return {
|
||||
"version": get_smsbot_version(),
|
||||
"owner": self.bot.owner_id,
|
||||
"subscribers": self.bot.subscriber_ids,
|
||||
"owners": self.bot.owners,
|
||||
"subscribers": self.bot.subscribers,
|
||||
}
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
@time(REQUEST_TIME)
|
||||
@validate_twilio_request
|
||||
def message(self):
|
||||
async def message(self):
|
||||
"""Handle incoming SMS messages from Twilio"""
|
||||
current_app.logger.info(
|
||||
"Received SMS from {From}: {Body}".format(**request.values.to_dict())
|
||||
)
|
||||
|
||||
message = "From: {From}\n\n{Body}".format(**request.values.to_dict())
|
||||
self.bot.send_subscribers(message)
|
||||
await self.bot.send_subscribers(
|
||||
TwilioMessage(request.values.to_dict()).to_markdownv2()
|
||||
)
|
||||
|
||||
# Return a blank response
|
||||
MESSAGE_COUNT.inc()
|
||||
return "<response></response>"
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>'
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
@time(REQUEST_TIME)
|
||||
@validate_twilio_request
|
||||
def call(self):
|
||||
async def call(self):
|
||||
"""Handle incoming calls from Twilio"""
|
||||
current_app.logger.info(
|
||||
"Received Call from {From}".format(**request.values.to_dict())
|
||||
)
|
||||
self.bot.send_subscribers(
|
||||
await self.bot.send_subscribers(
|
||||
"Received Call from {From}, rejecting.".format(**request.values.to_dict())
|
||||
)
|
||||
|
||||
# Always reject calls
|
||||
CALL_COUNT.inc()
|
||||
return "<Response><Reject/></Response>"
|
||||
|
||||
def serve(self, host="0.0.0.0", port=80, debug=False):
|
||||
serve(self.app, host=host, port=port)
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><Response><Reject/></Response>'
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
def test_import():
|
||||
import smsbot
|
||||
import smsbot.utils
|
||||
assert smsbot.utils.get_smsbot_version()
|
||||
69
tests/test_utils.py
Normal file
69
tests/test_utils.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from smsbot.utils import TwilioMessage
|
||||
|
||||
|
||||
def test_twiliomessage_normal():
|
||||
instance = TwilioMessage(
|
||||
{
|
||||
"From": "+1234567890",
|
||||
"To": "+0987654321",
|
||||
"Body": "Hello, world!",
|
||||
"NumMedia": "2",
|
||||
"MediaUrl0": "http://example.com/media1.jpg",
|
||||
"MediaUrl1": "http://example.com/media2.jpg",
|
||||
}
|
||||
)
|
||||
|
||||
assert instance.from_number == "+1234567890"
|
||||
assert instance.to_number == "+0987654321"
|
||||
assert instance.body == "Hello, world!"
|
||||
assert instance.media == [
|
||||
"http://example.com/media1.jpg",
|
||||
"http://example.com/media2.jpg",
|
||||
]
|
||||
|
||||
|
||||
def test_twiliomessage_no_media():
|
||||
instance = TwilioMessage(
|
||||
{
|
||||
"From": "+1234567890",
|
||||
"To": "+0987654321",
|
||||
"Body": "Hello, world!",
|
||||
}
|
||||
)
|
||||
|
||||
assert instance.media == []
|
||||
|
||||
|
||||
def test_twiliomessage_invalid_media_count():
|
||||
instance = TwilioMessage(
|
||||
{
|
||||
"From": "+1234567890",
|
||||
"To": "+0987654321",
|
||||
"Body": "Hello, world!",
|
||||
"NumMedia": "0",
|
||||
"MediaUrl0": "http://example.com/media1.jpg",
|
||||
"MediaUrl1": "http://example.com/media2.jpg",
|
||||
}
|
||||
)
|
||||
|
||||
assert instance.media == []
|
||||
|
||||
def test_twiliomessage_invalid_media_count_extra():
|
||||
instance = TwilioMessage(
|
||||
{
|
||||
"From": "+1234567890",
|
||||
"To": "+0987654321",
|
||||
"Body": "Hello, world!",
|
||||
"NumMedia": "5",
|
||||
"MediaUrl0": "http://example.com/media1.jpg",
|
||||
"MediaUrl1": "http://example.com/media2.jpg",
|
||||
}
|
||||
)
|
||||
|
||||
assert instance.media == [
|
||||
"http://example.com/media1.jpg",
|
||||
"http://example.com/media2.jpg",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
224
uv.lock
generated
224
uv.lock
generated
@@ -72,18 +72,30 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.6.3"
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytz" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "six" },
|
||||
{ name = "tzlocal" },
|
||||
{ name = "exceptiongroup" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/3d/f65972547c5aa533276ada2bea3c2ef51bb4c4de55b67a66129c111b89ad/APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244", size = 96309, upload-time = "2019-11-05T07:51:50.394Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/34/9ef20ed473c4fd2c3df54ef77a27ae3fc7500b16b192add4720cab8b2c09/APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526", size = 58881, upload-time = "2019-11-05T07:51:48.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -113,15 +125,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "4.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/ba/619250fa6bc11ce6aa4de0604d45843090a53cd7d10d7253b89669313370/cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff", size = 23682, upload-time = "2021-04-27T21:19:57.252Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/28/c4f5796c67ad06bb91d98d543a5e01805c1ff065e08871f78e52d2a331ad/cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", size = 11998, upload-time = "2021-04-27T21:19:55.559Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
@@ -202,6 +205,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
async = [
|
||||
{ name = "asgiref" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.7.0"
|
||||
@@ -228,6 +236,43 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -345,6 +390,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-async"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/38/70567994cce643dcd1845d52e8870d91e24b69f49f59381ed523c06d0650/prometheus_async-25.1.0.tar.gz", hash = "sha256:968e29b5255af57bc8146ade132e7aa0a21fe2b4d95ffefc7c732e9c3fbbb1dd", size = 36469, upload-time = "2025-02-08T10:24:20.505Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/45/ce7e7c879e7dbb1713f29a666e40285d3b25f7c431fa2c7a771aadc9183a/prometheus_async-25.1.0-py3-none-any.whl", hash = "sha256:4dfa4d85fbbf3fc052a87c7a221a68d4a1d6bd39d31723ac935304dacaedaf69", size = 18253, upload-time = "2025-02-08T10:24:17.23Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.22.1"
|
||||
@@ -455,27 +514,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-telegram-bot"
|
||||
version = "13.15"
|
||||
version = "22.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "apscheduler" },
|
||||
{ name = "cachetools" },
|
||||
{ name = "certifi" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tornado" },
|
||||
{ name = "httpx" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/bc/89cd44af162c3f5bf1bd50f4beb9f47353f027552e290899b55723746509/python-telegram-bot-13.15.tar.gz", hash = "sha256:b4047606b8081b62bbd6aa361f7ca1efe87fa8f1881ec9d932d35844bf57a154", size = 357762, upload-time = "2022-12-06T10:01:48.056Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/fc/0196e0d7ad247011a560788db204e0a28d76ab75b3d7c7131878f8fb5a06/python_telegram_bot-22.3.tar.gz", hash = "sha256:513d5ab9db96dcf25272dad0a726555e80edf60d09246a7d0d425b77115f5440", size = 1464513, upload-time = "2025-07-20T20:03:09.805Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/db/29356a2f43052af470993fdf2083ca8bf28da4586da836aa609de2d86a06/python_telegram_bot-13.15-py3-none-any.whl", hash = "sha256:06780c258d3f2a3c6c79a7aeb45714f4cd1dd6275941b7dc4628bba64fddd465", size = 519214, upload-time = "2022-12-06T10:01:43.082Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/54/0955bd46a1e046169500e129c7883664b6675d580074d68823485e4d5de1/python_telegram_bot-22.3-py3-none-any.whl", hash = "sha256:88fab2d1652dbfd5379552e8b904d86173c524fdb9270d3a8685f599ffe0299f", size = 717115, upload-time = "2025-07-20T20:03:07.261Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -519,34 +565,17 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||
]
|
||||
|
||||
[[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 = "smsbot"
|
||||
version = "0.0.7"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "flask", extra = ["async"] },
|
||||
{ name = "prometheus-async" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "python-telegram-bot" },
|
||||
{ name = "twilio" },
|
||||
{ name = "waitress" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -562,11 +591,12 @@ github = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "flask", specifier = ">=3.1.1" },
|
||||
{ name = "flask", extras = ["async"], specifier = ">=3.1.1" },
|
||||
{ name = "prometheus-async", specifier = ">=25.1.0" },
|
||||
{ name = "prometheus-client", specifier = ">=0.22.1" },
|
||||
{ name = "python-telegram-bot", specifier = "<20" },
|
||||
{ name = "python-telegram-bot", specifier = ">=22.3" },
|
||||
{ name = "twilio", specifier = ">=9.7.0" },
|
||||
{ name = "waitress", specifier = ">=3.0.2" },
|
||||
{ name = "uvicorn", specifier = ">=0.35.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -578,6 +608,15 @@ dev = [
|
||||
]
|
||||
github = [{ name = "pytest-github-actions-annotate-failures", specifier = ">=0.3.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
@@ -587,22 +626,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/44/cc9590db23758ee7906d40cacff06c02a21c2a6166602e095a56cbf2f6f6/tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", size = 497359, upload-time = "2020-10-30T20:17:51.882Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e1/ea4aa33216212beec8a20d134a3020648991a35f5d3b68a238654664b872/tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", size = 416573, upload-time = "2020-10-30T19:08:05.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6a/94ca5763b12a0fb784131d5d29ab9ebdc220d4050211ecad662393177840/tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", size = 426978, upload-time = "2020-10-30T22:53:20.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/75/c2d09f2e25834417f234cbd5b442b2bb8d6ef01009fe5936f24cc1ef66bb/tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", size = 427233, upload-time = "2020-10-30T22:53:21.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/22/cf57088b1b3ef17cb7eeddb256269c4309fc5a3fdb3a2b0ad535ed87c251/tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", size = 426979, upload-time = "2020-10-30T22:53:22.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b8/b2091d26482993f925d098b451ab5217a4565c56be4db2b67de6cf4921e4/tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", size = 427236, upload-time = "2020-10-30T22:53:24.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bb/1f3726d36c3f6a78304c7dc0ac6ca20739eaaf55d332b39f4715e048c48d/tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", size = 427701, upload-time = "2020-10-30T19:05:00.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/47/dd6ba18fce97a801632245479580e8e5081e629518caf4a82b6ff6607f40/tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", size = 421590, upload-time = "2020-10-30T20:22:34.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2d/4050006dd16f1cc4b8f3a83437b768dc849def37508aaf59b42a8a5907e4/tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", size = 422133, upload-time = "2020-10-30T20:22:35.58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.7.0"
|
||||
@@ -627,27 +650,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[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.5.0"
|
||||
@@ -658,12 +660,17 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waitress"
|
||||
version = "3.0.2"
|
||||
name = "uvicorn"
|
||||
version = "0.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -678,6 +685,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.17.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/be/be9b3b0a461ee3e30278706f3f3759b9b69afeedef7fe686036286c04ac6/wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc", size = 53485, upload-time = "2025-08-12T05:51:53.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a8/8f61d6b8f526efc8c10e12bf80b4206099fea78ade70427846a37bc9cbea/wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9", size = 38675, upload-time = "2025-08-12T05:51:42.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f1/23950c29a25637b74b322f9e425a17cc01a478f6afb35138ecb697f9558d/wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d", size = 38956, upload-time = "2025-08-12T05:52:03.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/46/dd0791943613885f62619f18ee6107e6133237a6b6ed8a9ecfac339d0b4f/wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a", size = 81745, upload-time = "2025-08-12T05:52:49.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ec/bb2d19bd1a614cc4f438abac13ae26c57186197920432d2a915183b15a8b/wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139", size = 82833, upload-time = "2025-08-12T05:52:27.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/eb/66579aea6ad36f07617fedca8e282e49c7c9bab64c63b446cfe4f7f47a49/wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df", size = 81889, upload-time = "2025-08-12T05:52:29.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/9c/a56b5ac0e2473bdc3fb11b22dd69ff423154d63861cf77911cdde5e38fd2/wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b", size = 81344, upload-time = "2025-08-12T05:52:50.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/4c/9bd735c42641d81cb58d7bfb142c58f95c833962d15113026705add41a07/wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81", size = 36462, upload-time = "2025-08-12T05:53:19.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ea/0b72f29cb5ebc16eb55c57dc0c98e5de76fc97f435fd407f7d409459c0a6/wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f", size = 38740, upload-time = "2025-08-12T05:53:18.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/8b/9eae65fb92321e38dbfec7719b87d840a4b92fde83fd1bbf238c5488d055/wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f", size = 36806, upload-time = "2025-08-12T05:52:58.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.20.1"
|
||||
|
||||
Reference in New Issue
Block a user