1 Commits

Author SHA1 Message Date
renovate[bot]
e50b6fdd5b chore(deps): update actions/checkout action to v5 2025-08-16 12:39:59 +00:00
11 changed files with 228 additions and 455 deletions

View File

@@ -13,7 +13,7 @@ jobs:
matrix: matrix:
python-version: ["3.9"] python-version: ["3.9"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Task - name: Install Task
uses: arduino/setup-task@v2 uses: arduino/setup-task@v2
with: with:

View File

@@ -2,6 +2,8 @@ name: Release
"on": "on":
push: push:
branches:
- main
tags: tags:
- "[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+"
@@ -10,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5

View File

@@ -14,9 +14,4 @@ tasks:
docker:build: docker:build:
desc: Build the container using Docker desc: Build the container using Docker
cmds: cmds:
- docker build . -t smsbot:latest - docker build . -t smsbot:latest
smsbot:run:
desc: Run the SMSBot
cmds:
- uv run smsbot

View File

@@ -1,18 +1,17 @@
[project] [project]
name = "smsbot" name = "smsbot"
version = "0.1.1" version = "0.0.7"
description = "A simple Telegram bot to receive SMS messages." description = "A simple Telegram bot to receive SMS messages."
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }] authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
license = { text = "MIT" } license = { text = "MIT" }
readme = "README.md" readme = "README.md"
requires-python = ">=3.9,<3.10" requires-python = ">=3.9,<3.10"
dependencies = [ dependencies = [
"flask[async]>=3.1.1", "flask>=3.1.1",
"prometheus-async>=25.1.0",
"prometheus-client>=0.22.1", "prometheus-client>=0.22.1",
"python-telegram-bot>=22.3", "python-telegram-bot<20",
"twilio>=9.7.0", "twilio>=9.7.0",
"uvicorn>=0.35.0", "waitress>=3.0.2",
] ]
[project.scripts] [project.scripts]

View File

@@ -1,11 +1,6 @@
import argparse import argparse
import asyncio
import logging import logging
import os import os
from signal import SIGINT, SIGTERM
import uvicorn
from asgiref.wsgi import WsgiToAsgi
from smsbot.telegram import TelegramSmsBot from smsbot.telegram import TelegramSmsBot
from smsbot.utils import get_smsbot_version from smsbot.utils import get_smsbot_version
@@ -18,7 +13,7 @@ def main():
"--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0" "--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0"
) )
parser.add_argument( parser.add_argument(
"--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or 80, type=int "--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or "80"
) )
parser.add_argument( parser.add_argument(
"--telegram-bot-token", default=os.environ.get("SMSBOT_TELEGRAM_BOT_TOKEN") "--telegram-bot-token", default=os.environ.get("SMSBOT_TELEGRAM_BOT_TOKEN")
@@ -27,74 +22,31 @@ def main():
parser.add_argument( parser.add_argument(
"--default-subscribers", default=os.environ.get("SMSBOT_DEFAULT_SUBSCRIBERS") "--default-subscribers", default=os.environ.get("SMSBOT_DEFAULT_SUBSCRIBERS")
) )
parser.add_argument("--debug", action="store_true") parser.add_argument("--log-level", default="INFO")
args = parser.parse_args() args = parser.parse_args()
if args.debug: # TODO: Replace for Py >=3.11
level = logging.DEBUG logging.basicConfig(level=logging.getLevelName(args.log_level))
else:
level = logging.INFO
logging.basicConfig(level=level)
logging.info("smsbot v%s", get_smsbot_version()) logging.info("smsbot v%s", get_smsbot_version())
logging.debug("Arguments: %s", args) logging.debug("Arguments: %s", args)
# Start bot # Start bot
telegram_bot = TelegramSmsBot(token=args.telegram_bot_token) telegram_bot = TelegramSmsBot(args.telegram_bot_token)
# Set the owner ID if configured # Set the owner ID if configured
if args.owner_id: if args.owner_id:
telegram_bot.owners = [int(args.owner_id)] telegram_bot.set_owner(args.owner_id)
else: else:
logging.warning("No Owner ID is set, which is not a good idea...") logging.warning("No Owner ID is set, which is not a good idea...")
# Add default subscribers # Add default subscribers
if args.default_subscribers: if args.default_subscribers:
for chat_id in args.default_subscribers.split(","): for chat_id in args.default_subscribers.split(","):
telegram_bot.subscribers.append(int(chat_id.strip())) telegram_bot.add_subscriber(chat_id)
telegram_bot.start()
# Start webhooks
webhooks = TwilioWebhookHandler() webhooks = TwilioWebhookHandler()
webhooks.set_bot(telegram_bot) 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()

View File

@@ -1,15 +1,9 @@
import logging import logging
from smsbot.utils import get_smsbot_version
from prometheus_client import Counter, Summary from prometheus_client import Counter, Summary
from telegram import Update from telegram.ext import CommandHandler, Updater
from telegram.ext import (
Application, from smsbot.utils import get_smsbot_version
ApplicationHandlerStop,
ContextTypes,
TypeHandler,
CommandHandler,
)
REQUEST_TIME = Summary( REQUEST_TIME = Summary(
"telegram_request_processing_seconds", "Time spent processing request" "telegram_request_processing_seconds", "Time spent processing request"
@@ -17,94 +11,108 @@ REQUEST_TIME = Summary(
COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands processed") COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands processed")
class TelegramSmsBot: class TelegramSmsBot(object):
def __init__(self, token: str, owners: list[int] = [], subscribers: list[int] = []): def __init__(
self, telegram_token, allow_subscribing=False, owner=None, subscribers=None
):
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.app = Application.builder().token(token).build() self.bot_token = telegram_token
self.owners = owners self.subscriber_ids = subscribers or []
self.subscribers = subscribers self.set_owner(owner)
self.init_handlers() 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))
def init_handlers(self): if allow_subscribing:
self.app.add_handler(TypeHandler(Update, self.callback), -1) self.updater.dispatcher.add_handler(
self.app.add_handler(CommandHandler("help", self.handler_help)) CommandHandler("subscribe", self.subscribe_handler)
self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe)) )
self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe)) self.updater.dispatcher.add_handler(
CommandHandler("unsubscribe", self.unsubscribe_handler)
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}"
) )
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): self.updater.dispatcher.add_error_handler(self.error_handler)
"""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"
)
async def send_subscribers(self, text: str): def start(self):
"""Send a message to all subscribers""" self.logger.info("Starting bot...")
for subscriber in self.subscribers: self.updater.start_polling()
self.logger.info(f"Sending message to subscriber {subscriber}") self.bot = self.updater.bot
await self.send_message(subscriber, text) self.logger.info("Bot Ready")
async def send_owners(self, text: str): def stop(self):
"""Send a message to all owners""" self.updater.stop()
for owner in self.owners:
self.logger.info(f"Sending message to owner {owner}")
await self.send_message(owner, text)
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_help(self, update, context): def help_handler(self, update, context):
"""Send a message when the command /help is issued.""" """Send a message when the command /help is issued."""
self.logger.info("/help command received in chat: %s", update.message.chat) self.logger.info("/help command received in chat: %s", update.message.chat)
commands = [] commands = []
for command in self.app.handlers[0]: for command in self.updater.dispatcher.handlers[0]:
if isinstance(command, CommandHandler): commands.extend(["/{0}".format(cmd) for cmd in command.command])
commands.extend(["/{0}".format(cmd) for cmd in command.commands])
await update.message.reply_markdown( update.message.reply_markdown(
"Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands)) "Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands))
) )
COMMAND_COUNT.inc() COMMAND_COUNT.inc()
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_subscribe( def subscribe_handler(self, update, context):
self, update: Update, context: ContextTypes.DEFAULT_TYPE self.logger.info("/subscribe command received")
): if update.message.chat["id"] not in self.subscriber_ids:
"""Handle subscription requests""" self.logger.info("{0} subscribed".format(update.message.chat["username"]))
user_id = update.effective_user.id self.subscriber_ids.append(update.message.chat["id"])
if user_id not in self.subscribers: self.send_owner(
self.subscribers.append(user_id) "{0} has subscribed".format(update.message.chat["username"])
self.logger.info(f"User {user_id} subscribed.") )
self.logger.info(f"Current subscribers: {self.subscribers}") update.message.reply_markdown(
await update.message.reply_markdown( "You have been subscribed to SMS notifications"
"You have successfully subscribed to updates."
) )
else: else:
self.logger.info(f"User {user_id} is already subscribed.") update.message.reply_markdown(
"You are already subscribed to SMS notifications"
)
COMMAND_COUNT.inc()
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_unsubscribe( def unsubscribe_handler(self, update, context):
self, update: Update, context: ContextTypes.DEFAULT_TYPE self.logger.info("/unsubscribe command received")
): if update.message.chat["id"] in self.subscriber_ids:
"""Handle unsubscription requests""" self.logger.info("{0} unsubscribed".format(update.message.chat["username"]))
user_id = update.effective_user.id self.subscriber_ids.remove(update.message.chat["id"])
if user_id in self.subscribers: self.send_owner(
self.subscribers.remove(user_id) "{0} has unsubscribed".format(update.message.chat["username"])
self.logger.info(f"User {user_id} unsubscribed.") )
self.logger.info(f"Current subscribers: {self.subscribers}") update.message.reply_markdown(
await update.message.reply_markdown( "You have been unsubscribed to SMS notifications"
"You have successfully unsubscribed from updates."
) )
else: else:
self.logger.info(f"User {user_id} is not subscribed.") 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)

View File

@@ -3,88 +3,3 @@ from importlib.metadata import version
def get_smsbot_version(): def get_smsbot_version():
return version("smsbot") return version("smsbot")
class TwilioWebhookPayload:
@staticmethod
def parse(data: dict):
"""Return the correct class for the incoming Twilio webhook payload"""
if "SmsMessageSid" in data:
return TwilioMessage(data)
if "CallSid" in data:
return TwilioCall(data)
def _escape(self, text: str) -> str:
"""Escape text for MarkdownV2"""
characters = [
"_",
"*",
"[",
"]",
"(",
")",
"~",
"`",
">",
"#",
"+",
"-",
"=",
"|",
"{",
"}",
".",
"!",
]
for char in characters:
text = text.replace(char, rf"\{char}")
return text
class TwilioMessage(TwilioWebhookPayload):
"""Represents a Twilio SMS 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 __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
class TwilioCall(TwilioWebhookPayload):
"""Represents a Twilio voice call"""
def __init__(self, data: dict) -> None:
self.from_number: str = data.get("From", "Unknown")
self.to_number: str = data.get("To", "Unknown")
def __repr__(self) -> str:
return f"TwilioCall(from={self.from_number}, to={self.to_number})"
def to_str(self) -> str:
msg = f"Call from {self.from_number}, rejected."
return msg
def to_markdownv2(self):
msg = f"Call from {self._escape(self.from_number)}, rejected\."
return msg

View File

@@ -2,16 +2,14 @@ import os
from functools import wraps from functools import wraps
from flask import Flask, abort, current_app, request from flask import Flask, abort, current_app, request
from prometheus_async.aio import time
from prometheus_client import Counter, Summary, make_wsgi_app from prometheus_client import Counter, Summary, make_wsgi_app
from twilio.request_validator import RequestValidator from twilio.request_validator import RequestValidator
from waitress import serve
from werkzeug.middleware.dispatcher import DispatcherMiddleware from werkzeug.middleware.dispatcher import DispatcherMiddleware
from smsbot.utils import TwilioWebhookPayload, get_smsbot_version from smsbot.utils import get_smsbot_version
REQUEST_TIME = Summary( REQUEST_TIME = Summary("webhook_request_processing_seconds", "Time spent processing request")
"webhook_request_processing_seconds", "Time spent processing request"
)
MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed") MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed")
CALL_COUNT = Counter("webhook_call_count", "Total number of calls processed") CALL_COUNT = Counter("webhook_call_count", "Total number of calls processed")
@@ -20,7 +18,7 @@ def validate_twilio_request(func):
"""Validates that incoming requests genuinely originated from Twilio""" """Validates that incoming requests genuinely originated from Twilio"""
@wraps(func) @wraps(func)
async def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs): # noqa: WPS430
# Create an instance of the RequestValidator class # Create an instance of the RequestValidator class
twilio_token = os.environ.get("SMSBOT_TWILIO_AUTH_TOKEN") twilio_token = os.environ.get("SMSBOT_TWILIO_AUTH_TOKEN")
@@ -28,7 +26,7 @@ def validate_twilio_request(func):
current_app.logger.warning( current_app.logger.warning(
"Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing" "Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing"
) )
return await func(*args, **kwargs) return func(*args, **kwargs)
validator = RequestValidator(twilio_token) validator = RequestValidator(twilio_token)
@@ -65,47 +63,47 @@ class TwilioWebhookHandler(object):
}, },
) )
def set_bot(self, bot): def set_bot(self, bot): # noqa: WPS615
self.bot = bot self.bot = bot
async def index(self): def index(self):
return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>' return ""
async def health(self): @REQUEST_TIME.time()
"""Return basic health information""" def health(self):
return { return {
"version": get_smsbot_version(), "version": get_smsbot_version(),
"owners": self.bot.owners, "owner": self.bot.owner_id,
"subscribers": self.bot.subscribers, "subscribers": self.bot.subscriber_ids,
} }
@time(REQUEST_TIME) @REQUEST_TIME.time()
@validate_twilio_request @validate_twilio_request
async def message(self): def message(self):
"""Handle incoming SMS messages from Twilio"""
current_app.logger.info( current_app.logger.info(
"Received SMS from {From}: {Body}".format(**request.values.to_dict()) "Received SMS from {From}: {Body}".format(**request.values.to_dict())
) )
await self.bot.send_subscribers( message = "From: {From}\n\n{Body}".format(**request.values.to_dict())
TwilioWebhookPayload.parse(request.values.to_dict()).to_markdownv2() self.bot.send_subscribers(message)
)
# Return a blank response # Return a blank response
MESSAGE_COUNT.inc() MESSAGE_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' return "<response></response>"
@time(REQUEST_TIME) @REQUEST_TIME.time()
@validate_twilio_request @validate_twilio_request
async def call(self): def call(self):
"""Handle incoming calls from Twilio"""
current_app.logger.info( current_app.logger.info(
"Received Call from {From}".format(**request.values.to_dict()) "Received Call from {From}".format(**request.values.to_dict())
) )
await self.bot.send_subscribers( self.bot.send_subscribers(
TwilioWebhookPayload.parse(request.values.to_dict()).to_markdownv2() "Received Call from {From}, rejecting.".format(**request.values.to_dict())
) )
# Always reject calls # Always reject calls
CALL_COUNT.inc() CALL_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response><Reject/></Response>' return "<Response><Reject/></Response>"
def serve(self, host="0.0.0.0", port=80, debug=False):
serve(self.app, host=host, port=port)

View File

@@ -1,3 +1,2 @@
def test_import(): def test_import():
import smsbot.utils import smsbot
assert smsbot.utils.get_smsbot_version()

View File

@@ -1,69 +0,0 @@
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
View File

@@ -72,30 +72,18 @@ wheels = [
] ]
[[package]] [[package]]
name = "anyio" name = "apscheduler"
version = "4.10.0" version = "3.6.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "exceptiongroup" }, { name = "pytz" },
{ name = "idna" }, { name = "setuptools" },
{ name = "sniffio" }, { name = "six" },
{ name = "typing-extensions" }, { name = "tzlocal" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
]
[[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]] [[package]]
@@ -125,6 +113,15 @@ 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" }, { 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]] [[package]]
name = "certifi" name = "certifi"
version = "2025.8.3" version = "2025.8.3"
@@ -205,11 +202,6 @@ 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" }, { 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]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.7.0" version = "1.7.0"
@@ -236,43 +228,6 @@ 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" }, { 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]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@@ -390,20 +345,6 @@ 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" }, { 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]] [[package]]
name = "prometheus-client" name = "prometheus-client"
version = "0.22.1" version = "0.22.1"
@@ -514,14 +455,27 @@ wheels = [
[[package]] [[package]]
name = "python-telegram-bot" name = "python-telegram-bot"
version = "22.3" version = "13.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "apscheduler" },
{ name = "cachetools" },
{ name = "certifi" },
{ name = "pytz" },
{ name = "tornado" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
] ]
[[package]] [[package]]
@@ -565,17 +519,34 @@ 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" }, { 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]] [[package]]
name = "smsbot" name = "smsbot"
version = "0.1.1" version = "0.0.7"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "flask", extra = ["async"] }, { name = "flask" },
{ name = "prometheus-async" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "python-telegram-bot" }, { name = "python-telegram-bot" },
{ name = "twilio" }, { name = "twilio" },
{ name = "uvicorn" }, { name = "waitress" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -591,12 +562,11 @@ github = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "flask", extras = ["async"], specifier = ">=3.1.1" }, { name = "flask", specifier = ">=3.1.1" },
{ name = "prometheus-async", specifier = ">=25.1.0" },
{ name = "prometheus-client", specifier = ">=0.22.1" }, { name = "prometheus-client", specifier = ">=0.22.1" },
{ name = "python-telegram-bot", specifier = ">=22.3" }, { name = "python-telegram-bot", specifier = "<20" },
{ name = "twilio", specifier = ">=9.7.0" }, { name = "twilio", specifier = ">=9.7.0" },
{ name = "uvicorn", specifier = ">=0.35.0" }, { name = "waitress", specifier = ">=3.0.2" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -608,15 +578,6 @@ dev = [
] ]
github = [{ name = "pytest-github-actions-annotate-failures", specifier = ">=0.3.0" }] 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]] [[package]]
name = "tomli" name = "tomli"
version = "2.2.1" version = "2.2.1"
@@ -626,6 +587,22 @@ 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" }, { 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]] [[package]]
name = "twilio" name = "twilio"
version = "9.7.0" version = "9.7.0"
@@ -650,6 +627,27 @@ 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" }, { 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]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.5.0"
@@ -660,17 +658,12 @@ wheels = [
] ]
[[package]] [[package]]
name = "uvicorn" name = "waitress"
version = "0.35.0" version = "3.0.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" }
{ 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 = [ wheels = [
{ 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" }, { 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" },
] ]
[[package]] [[package]]
@@ -685,25 +678,6 @@ 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" }, { 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]] [[package]]
name = "yarl" name = "yarl"
version = "1.20.1" version = "1.20.1"