diff --git a/.env.dist b/.env.dist index c9401d2..eef0d9b 100644 --- a/.env.dist +++ b/.env.dist @@ -1,5 +1,11 @@ -LUCKY_POT_DISCORD_TOKEN= -LUCKY_POT_STACKCOIN_BOT_TOKEN= -LUCKY_POT_STACKCOIN_BASE_URL= -LUCKY_POT_TESTING_GUILD_ID= -LUCKY_POT_DEBUG_MODE= \ No newline at end of file +LUCKYPOT_DISCORD_TOKEN= +LUCKYPOT_STACKCOIN_API_URL=http://localhost:4000 +LUCKYPOT_STACKCOIN_API_TOKEN= +LUCKYPOT_STACKCOIN_WS_URL=ws://localhost:4000/ws +LUCKYPOT_DB_PATH=luckypot.db +LUCKYPOT_TESTING_GUILD_ID= +LUCKYPOT_DEBUG_MODE=false +LUCKYPOT_DAILY_DRAW_HOUR=0 +LUCKYPOT_DAILY_DRAW_MINUTE=0 +# Set >0 to override daily schedule with a repeating interval (for testing) +# LUCKYPOT_DRAW_INTERVAL_MINUTES=5 diff --git a/README.md b/README.md index cd6a889..068d634 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,41 @@ # LuckyPot -Discord bot that implements a lottery system on-top of StackCoin. +A lottery bot for [StackCoin](https://github.com/StackCoin/StackCoin). Players +enter a pot by paying STK, and a daily draw selects a winner who takes the +entire pot. There's also a 5% chance of an instant win on entry. -Built using [Hikari](https://www.hikari-py.dev/), and [stackcoin-python](https://github.com/StackCoin/stackcoin-python). +## Setup +```bash +# Install dependencies +uv sync + +# Copy and fill in environment variables +cp .env.dist .env + +# Run the bot +python lucky_pot.py +``` + +## Environment Variables + +All environment variables are prefixed with `LUCKYPOT_`. + +| Variable | Required | Default | Description | +| --------------------------- | -------- | -------------------------------------- | ------------------------------------ | +| `LUCKYPOT_DISCORD_TOKEN` | Yes | | Discord bot token | +| `LUCKYPOT_STACKCOIN_API_URL`| Yes | `http://localhost:4000` | StackCoin API base URL | +| `LUCKYPOT_STACKCOIN_API_TOKEN`| Yes | | Bot API token from StackCoin | +| `LUCKYPOT_STACKCOIN_WS_URL` | No | `ws://localhost:4000/ws` | StackCoin WebSocket URL | +| `LUCKYPOT_DB_PATH` | No | `luckypot.db` | SQLite database path | +| `LUCKYPOT_TESTING_GUILD_ID` | No | | Restrict slash commands to one guild | +| `LUCKYPOT_DEBUG_MODE` | No | `false` | Enable `/force-end-pot` command | +| `LUCKYPOT_DAILY_DRAW_HOUR` | No | `0` | Daily draw hour (UTC) | +| `LUCKYPOT_DAILY_DRAW_MINUTE`| No | `0` | Daily draw minute (UTC) | + +## Slash Commands + +- `/enter-pot` — Enter the daily lucky pot (costs 5 STK) +- `/pot-status` — Check the current pot status and participants +- `/pot-history` — View recent pot winners +- `/force-end-pot` — [DEBUG] Force end the current pot with a draw diff --git a/config.py b/config.py deleted file mode 100644 index f1edddc..0000000 --- a/config.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from dotenv import load_dotenv - -load_dotenv() - -DISCORD_TOKEN = os.getenv("LUCKY_POT_DISCORD_TOKEN") -TESTING_GUILD_ID = os.getenv("LUCKY_POT_TESTING_GUILD_ID") -DEBUG_MODE = os.getenv("LUCKY_POT_DEBUG_MODE", "false").lower() == "true" - -STACKCOIN_BOT_TOKEN = os.getenv("LUCKY_POT_STACKCOIN_BOT_TOKEN") -STACKCOIN_BASE_URL = ( - os.getenv("LUCKY_POT_STACKCOIN_BASE_URL") or "https://stackcoin.world" -) diff --git a/db.py b/db.py deleted file mode 100644 index 02ff743..0000000 --- a/db.py +++ /dev/null @@ -1,368 +0,0 @@ -import sqlite3 -from contextlib import contextmanager -from typing import Generator -from typing_extensions import TypedDict - - -class Pot(TypedDict): - pot_id: int - guild_id: str - winner_id: str | None - winning_amount: int - created_at: str - won_at: str | None - is_active: bool - - -class PotEntry(TypedDict): - entry_id: int - pot_id: int - discord_id: str - guild_id: str - amount: int - status: str - stackcoin_request_id: str - created_at: str - confirmed_at: str | None - pot_guild_id: str - - -class Participant(TypedDict): - discord_id: str - - -class ParticipantWithAmount(TypedDict): - discord_id: str - total_amount: int - - -class PotStatus(TypedDict): - pot_id: int - total_amount: int - participant_count: int - participants: list[ParticipantWithAmount] - created_at: str - - -DB_PATH = "lucky_pot.db" - - -@contextmanager -def get_connection() -> Generator[sqlite3.Connection, None, None]: - """Get a database connection with automatic cleanup""" - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - try: - yield conn - finally: - conn.close() - - -@contextmanager -def get_transaction() -> Generator[sqlite3.Connection, None, None]: - """Get a database connection with transaction management""" - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - try: - yield conn - conn.commit() - except Exception: - conn.rollback() - raise - finally: - conn.close() - - -def init_database(): - with get_connection() as conn: - conn.execute(""" - CREATE TABLE IF NOT EXISTS users ( - discord_id TEXT NOT NULL, - guild_id TEXT NOT NULL, - total_wins INTEGER DEFAULT 0, - total_winnings INTEGER DEFAULT 0, - last_entry_time TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (discord_id, guild_id) - ) - """) - - conn.execute(""" - CREATE TABLE IF NOT EXISTS pots ( - pot_id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT NOT NULL, - winner_id TEXT NULL, - winning_amount INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - won_at TIMESTAMP NULL, - is_active BOOLEAN DEFAULT TRUE - ) - """) - - conn.execute(""" - CREATE TABLE IF NOT EXISTS pot_entries ( - entry_id INTEGER PRIMARY KEY AUTOINCREMENT, - pot_id INTEGER NOT NULL, - discord_id TEXT NOT NULL, - guild_id TEXT NOT NULL, - amount INTEGER NOT NULL DEFAULT 5, - status TEXT DEFAULT 'unconfirmed', - stackcoin_request_id TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - confirmed_at TIMESTAMP NULL, - FOREIGN KEY (pot_id) REFERENCES pots(pot_id), - FOREIGN KEY (discord_id, guild_id) REFERENCES users(discord_id, guild_id), - UNIQUE(pot_id, discord_id) - ) - """) - - conn.execute(""" - CREATE TABLE IF NOT EXISTS stackcoin_requests ( - request_id TEXT PRIMARY KEY, - entry_id INTEGER NOT NULL, - status TEXT DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - processed_at TIMESTAMP NULL, - FOREIGN KEY (entry_id) REFERENCES pot_entries(entry_id) - ) - """) - - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_pots_guild_active ON pots(guild_id, is_active)" - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_entries_pot_status ON pot_entries(pot_id, status)" - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_entries_user_time ON pot_entries(discord_id, guild_id, created_at)" - ) - - conn.commit() - - -def get_or_create_user( - conn: sqlite3.Connection, discord_id: str, guild_id: str -) -> None: - conn.execute( - """ - INSERT OR IGNORE INTO users (discord_id, guild_id) - VALUES (?, ?) - """, - (discord_id, guild_id), - ) - - -def get_current_pot(conn: sqlite3.Connection, guild_id: str) -> Pot | None: - cursor = conn.execute( - """ - SELECT * FROM pots - WHERE guild_id = ? AND is_active = TRUE AND winner_id IS NULL - ORDER BY created_at DESC LIMIT 1 - """, - (guild_id,), - ) - row = cursor.fetchone() - return Pot(**dict(row)) if row else None - - -def create_new_pot(conn: sqlite3.Connection, guild_id: str) -> int: - cursor = conn.execute( - """ - INSERT INTO pots (guild_id) VALUES (?) - """, - (guild_id,), - ) - last_row_id = cursor.lastrowid - if last_row_id is None: - raise Exception("Failed to create new pot") - return last_row_id - - -def can_user_enter_pot( - conn: sqlite3.Connection, discord_id: str, guild_id: str, pot_id: int -) -> bool: - cursor = conn.execute( - """ - SELECT COUNT(*) FROM pot_entries - WHERE discord_id = ? AND guild_id = ? AND pot_id = ? - AND status IN ('confirmed', 'unconfirmed', 'instant_win') - """, - (discord_id, guild_id, pot_id), - ) - count = cursor.fetchone()[0] - return count == 0 - - -def create_pot_entry( - conn: sqlite3.Connection, - pot_id: int, - discord_id: str, - guild_id: str, - stackcoin_request_id: str, - is_instant_win: bool = False, -) -> int: - cursor = conn.execute( - """ - INSERT INTO pot_entries (pot_id, discord_id, guild_id, stackcoin_request_id) - VALUES (?, ?, ?, ?) - """, - (pot_id, discord_id, guild_id, stackcoin_request_id), - ) - - entry_id = cursor.lastrowid - if entry_id is None: - raise Exception("Failed to create pot entry") - - if is_instant_win: - cursor.execute( - """ - UPDATE pot_entries SET status = 'instant_win' WHERE entry_id = ? - """, - (entry_id,), - ) - - return entry_id - - -def get_pot_status(conn: sqlite3.Connection, guild_id: str) -> PotStatus | None: - pot = get_current_pot(conn, guild_id) - if not pot: - return None - - cursor = conn.execute( - """ - SELECT discord_id, amount as total_amount - FROM pot_entries - WHERE pot_id = ? AND status = 'confirmed' - ORDER BY total_amount DESC - """, - (pot["pot_id"],), - ) - - participants = [ParticipantWithAmount(**dict(row)) for row in cursor.fetchall()] - - cursor = conn.execute( - """ - SELECT SUM(amount) as total_pot FROM pot_entries - WHERE pot_id = ? AND status = 'confirmed' - """, - (pot["pot_id"],), - ) - - total_pot = cursor.fetchone()[0] or 0 - - return PotStatus( - pot_id=pot["pot_id"], - total_amount=total_pot, - participant_count=len(participants), - participants=participants, - created_at=pot["created_at"], - ) - - -def confirm_entry(conn: sqlite3.Connection, entry_id: int) -> None: - conn.execute( - """ - UPDATE pot_entries - SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP - WHERE entry_id = ? - """, - (entry_id,), - ) - - -def deny_entry(conn: sqlite3.Connection, entry_id: int) -> None: - conn.execute( - """ - UPDATE pot_entries - SET status = 'denied' - WHERE entry_id = ? - """, - (entry_id,), - ) - - -def get_unconfirmed_entries(conn: sqlite3.Connection) -> list[PotEntry]: - cursor = conn.execute(""" - SELECT pe.*, p.guild_id as pot_guild_id - FROM pot_entries pe - JOIN pots p ON pe.pot_id = p.pot_id - WHERE pe.status = 'unconfirmed' - AND pe.created_at > datetime('now', '-1 hour') - ORDER BY pe.created_at ASC - """) - return [PotEntry(**dict(row)) for row in cursor.fetchall()] - - -def get_expired_entries(conn: sqlite3.Connection) -> list[PotEntry]: - cursor = conn.execute(""" - SELECT pe.*, p.guild_id as pot_guild_id - FROM pot_entries pe - JOIN pots p ON pe.pot_id = p.pot_id - WHERE pe.status = 'unconfirmed' - AND pe.created_at <= datetime('now', '-1 hour') - ORDER BY pe.created_at ASC - """) - return [PotEntry(**dict(row)) for row in cursor.fetchall()] - - -def get_active_pot_participants( - conn: sqlite3.Connection, guild_id: str -) -> list[Participant]: - pot = get_current_pot(conn, guild_id) - if not pot: - return [] - - cursor = conn.execute( - """ - SELECT discord_id - FROM pot_entries - WHERE pot_id = ? AND status = 'confirmed' - """, - (pot["pot_id"],), - ) - return [Participant(**dict(row)) for row in cursor.fetchall()] - - -def win_pot( - conn: sqlite3.Connection, guild_id: str, winner_id: str, winning_amount: int -) -> None: - conn.execute( - """ - UPDATE pots - SET winner_id = ?, winning_amount = ?, won_at = CURRENT_TIMESTAMP, is_active = FALSE - WHERE guild_id = ? AND is_active = TRUE AND winner_id IS NULL - """, - (winner_id, winning_amount, guild_id), - ) - - conn.execute( - """ - UPDATE users - SET total_wins = total_wins + 1, total_winnings = total_winnings + ? - WHERE discord_id = ? AND guild_id = ? - """, - (winning_amount, winner_id, guild_id), - ) - - -def get_all_active_guilds(conn: sqlite3.Connection) -> list[str]: - cursor = conn.execute(""" - SELECT DISTINCT guild_id FROM pots WHERE is_active = TRUE - """) - return [row[0] for row in cursor.fetchall()] - - -def get_entry_by_id(conn: sqlite3.Connection, entry_id: int) -> PotEntry | None: - """Get a pot entry by ID""" - cursor = conn.execute( - """ - SELECT pe.*, p.guild_id as pot_guild_id - FROM pot_entries pe - JOIN pots p ON pe.pot_id = p.pot_id - WHERE pe.entry_id = ? - """, - (entry_id,), - ) - row = cursor.fetchone() - return PotEntry(**dict(row)) if row else None diff --git a/docs/plans/2026-03-04-discord-bot-layer.md b/docs/plans/2026-03-04-discord-bot-layer.md new file mode 100644 index 0000000..5f91234 --- /dev/null +++ b/docs/plans/2026-03-04-discord-bot-layer.md @@ -0,0 +1,813 @@ +# Discord Bot Layer Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Re-add the hikari/lightbulb Discord bot (slash commands, rich UI, channel announcements) on top of the framework-agnostic `luckypot/` core package, with an asyncio-based daily draw scheduler. + +**Architecture:** A `luckypot/discord/` subpackage contains all Discord-specific code (bot lifecycle, slash commands, UI builders, channel announcements). The entry point `lucky_pot.py` wires the bot, StackCoin WebSocket gateway, and daily draw scheduler together. The core `luckypot/` package remains untouched — E2E tests continue importing `luckypot.game`, `luckypot.db`, `luckypot.stk` without triggering any hikari imports. + +**Tech Stack:** Python 3.13, hikari, hikari-lightbulb, httpx, websockets, loguru, SQLite + +--- + +### Task 1: Clean up dependencies and config + +**Files:** +- Modify: `pyproject.toml` +- Modify: `luckypot/config.py` +- Modify: `.env.dist` +- Delete: `config.py` (root shim) +- Delete: `db.py` (root shim) +- Delete: `stk.py` (root shim) + +**Step 1: Update pyproject.toml** + +Remove `schedule` dependency. Keep everything else. + +```toml +[project] +name = "luckypot" +version = "0.1.0" +description = "LuckyPot is a bot that implements a lottery system on top of StackCoin" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "hikari>=2.3.5", + "hikari-lightbulb>=3.1.1", + "httpx>=0.27.0", + "loguru>=0.7.3", + "python-dotenv>=1.1.1", + "websockets>=13.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["luckypot"] +``` + +**Step 2: Add new config variables to `luckypot/config.py`** + +```python +"""Configuration loading from environment variables.""" +import os +from dotenv import load_dotenv + +load_dotenv() + +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "") +STACKCOIN_API_URL = os.getenv("STACKCOIN_API_URL", "http://localhost:4000") +STACKCOIN_API_TOKEN = os.getenv("STACKCOIN_API_TOKEN", "") +STACKCOIN_WS_URL = os.getenv("STACKCOIN_WS_URL", "ws://localhost:4000/ws") +DB_PATH = os.getenv("LUCKYPOT_DB_PATH", "luckypot.db") + +# Discord bot settings +TESTING_GUILD_ID = os.getenv("TESTING_GUILD_ID", "") +DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true" + +# Daily draw schedule (UTC) +DAILY_DRAW_HOUR = int(os.getenv("DAILY_DRAW_HOUR", "0")) +DAILY_DRAW_MINUTE = int(os.getenv("DAILY_DRAW_MINUTE", "0")) +``` + +**Step 3: Update `.env.dist`** + +``` +DISCORD_TOKEN= +STACKCOIN_API_URL=http://localhost:4000 +STACKCOIN_API_TOKEN= +STACKCOIN_WS_URL=ws://localhost:4000/ws +LUCKYPOT_DB_PATH=luckypot.db +TESTING_GUILD_ID= +DEBUG_MODE=false +DAILY_DRAW_HOUR=0 +DAILY_DRAW_MINUTE=0 +``` + +**Step 4: Delete root-level shim files** + +```bash +rm config.py db.py stk.py +``` + +**Step 5: Run E2E tests from the StackCoin repo to confirm core package still works** + +```bash +cd /path/to/StackCoin/test/e2e && python -m pytest test_luckypot.py -x -v +``` + +Expected: All luckypot tests pass (they only import from `luckypot.game`, `luckypot.db`, `luckypot.stk`). + +**Step 6: Commit** + +```bash +git add -A && git commit -m "chore: clean up deps, add config vars, remove root shims" +``` + +--- + +### Task 2: Create `luckypot/discord/ui.py` — rich container builders + +This module has zero side effects — pure functions that build hikari container components. Easy to write and test in isolation. + +**Files:** +- Create: `luckypot/discord/__init__.py` +- Create: `luckypot/discord/ui.py` + +**Step 1: Create the `__init__.py`** + +```python +"""Discord bot layer for LuckyPot.""" +``` + +**Step 2: Create `ui.py`** + +This module provides builder functions that return hikari `ContainerComponentBuilder` objects. Each function takes plain data dicts (the same dicts returned by `luckypot.db` and `luckypot.game`) and builds a rich container response. + +```python +"""Rich container UI builders for Discord responses.""" +from datetime import datetime, timedelta, timezone + +import hikari +from hikari.impl.special_endpoints import ContainerComponentBuilder + +from luckypot import config + + +def build_entry_pending(request_id: int, amount: int) -> ContainerComponentBuilder: + """Build response for a successful pot entry (pending payment).""" + container = ContainerComponentBuilder(accent_color=hikari.Color(0xFFA500)) + container.add_text_display("🎲 Pot Entry Submitted!") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display( + f"Accept the **{amount} STK** payment request from StackCoin via DMs to confirm your spot." + ) + return container + + +def build_entry_instant_win() -> ContainerComponentBuilder: + """Build response for an instant win roll (still needs payment).""" + container = ContainerComponentBuilder(accent_color=hikari.Color(0xFFD700)) + container.add_text_display("🎉 INSTANT WIN!") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display( + "You rolled an **instant win**! Accept the payment request to claim the entire pot!" + ) + return container + + +def build_entry_already_entered() -> ContainerComponentBuilder: + """Build response when user has already entered the pot.""" + container = ContainerComponentBuilder(accent_color=hikari.Color(0xFF0000)) + container.add_text_display("❌ Already Entered") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display("You have already entered this pot! You can only enter once per pot.") + return container + + +def build_entry_error(message: str) -> ContainerComponentBuilder: + """Build response for a pot entry error.""" + container = ContainerComponentBuilder(accent_color=hikari.Color(0xFF0000)) + container.add_text_display("❌ Error") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(message) + return container + + +def build_pot_status(status: dict) -> ContainerComponentBuilder: + """Build the pot status display. + + Args: + status: Dict from ``db.get_pot_status()`` with keys: + active, participants, total_amount, and optionally pot_id. + """ + if not status.get("active"): + container = ContainerComponentBuilder(accent_color=hikari.Color(0x808080)) + container.add_text_display("🎲 No Active Pot") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display("Use `/enter-pot` to start one!") + return container + + container = ContainerComponentBuilder(accent_color=hikari.Color(0x00FF00)) + container.add_text_display("🎰 Lucky Pot Status") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(f"💰 Total Pot: **{status['total_amount']} STK**") + container.add_text_display(f"👥 Participants: **{status['participants']}**") + + # Next daily draw countdown + now = datetime.now(timezone.utc) + next_draw = now.replace( + hour=config.DAILY_DRAW_HOUR, + minute=config.DAILY_DRAW_MINUTE, + second=0, + microsecond=0, + ) + if next_draw <= now: + next_draw += timedelta(days=1) + container.add_text_display(f"⏰ Next Draw: ") + + if "pot_id" in status: + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(f"Pot ID: {status['pot_id']}") + + return container + + +def build_pot_history(history: list[dict]) -> ContainerComponentBuilder: + """Build the pot history display. + + Args: + history: List of dicts from ``db.get_pot_history()``. + """ + container = ContainerComponentBuilder(accent_color=hikari.Color(0x4169E1)) + container.add_text_display("📜 Pot History") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + + if not history: + container.add_text_display("No completed pots yet.") + return container + + for pot in history: + winner = pot.get("winner_discord_id") + amount = pot.get("winning_amount", 0) + win_type = pot.get("win_type", "DRAW") + ended = pot.get("ended_at", "?") + winner_text = f"<@{winner}>" if winner else "No winner" + container.add_text_display(f"**{amount} STK** → {winner_text} ({win_type}) — {ended}") + + return container + + +def build_winner_announcement( + winner_discord_id: str, winning_amount: int, win_type: str +) -> str: + """Build a winner announcement message for the guild channel. + + Returns a plain string (not a container) since channel announcements + should be simple text that renders well for everyone. + """ + return ( + f"🎉 **{win_type} WINNER!** 🎉\n\n" + f"<@{winner_discord_id}> has won the pot of **{winning_amount} STK**!\n" + f"Congratulations! 🎊\n\n" + f"A new pot has started — use `/enter-pot` to join!" + ) + + +def build_no_winner_announcement(pot_amount: int) -> str: + """Build a daily draw 'no winner' announcement.""" + return ( + f"🎲 Daily draw occurred, but the pot continues! No winner this time.\n" + f"Current pot: **{pot_amount} STK**\n" + f"Use `/enter-pot` to increase your chances!" + ) +``` + +**Step 3: Commit** + +```bash +git add -A && git commit -m "feat: add Discord UI container builders" +``` + +--- + +### Task 3: Create `luckypot/discord/scheduler.py` — daily draw loop + +**Files:** +- Create: `luckypot/discord/scheduler.py` + +**Step 1: Create `scheduler.py`** + +```python +"""Asyncio-based daily draw scheduler.""" +import asyncio +from datetime import datetime, timedelta, timezone + +from loguru import logger + +from luckypot import config +from luckypot.game import daily_pot_draw, AnnounceFn + + +async def run_daily_draw_loop(announce_fn: AnnounceFn = None): + """Run the daily pot draw at the configured UTC time. + + Runs forever, sleeping until the next draw time, then calling + ``game.daily_pot_draw()``. If the draw time has already passed + today, waits until tomorrow. + """ + while True: + now = datetime.now(timezone.utc) + next_draw = now.replace( + hour=config.DAILY_DRAW_HOUR, + minute=config.DAILY_DRAW_MINUTE, + second=0, + microsecond=0, + ) + if next_draw <= now: + next_draw += timedelta(days=1) + + sleep_seconds = (next_draw - now).total_seconds() + logger.info(f"Next daily draw at {next_draw.isoformat()} (in {sleep_seconds:.0f}s)") + await asyncio.sleep(sleep_seconds) + + logger.info("Running daily pot draw...") + try: + await daily_pot_draw(announce_fn=announce_fn) + except Exception as e: + logger.error(f"Daily draw failed: {e}") +``` + +**Step 2: Commit** + +```bash +git add -A && git commit -m "feat: add asyncio daily draw scheduler" +``` + +--- + +### Task 4: Create `luckypot/discord/bot.py` — bot lifecycle and announcements + +**Files:** +- Create: `luckypot/discord/bot.py` + +**Step 1: Create `bot.py`** + +This module creates the hikari bot, lightbulb client, and provides the `announce_fn` factory that posts to guild channels via the StackCoin API's designated channel lookup. + +```python +"""Hikari bot lifecycle, lightbulb client setup, and channel announcements.""" +import asyncio +from typing import Callable, Awaitable + +import hikari +import lightbulb +from loguru import logger + +from luckypot import config, stk + + +def create_bot() -> hikari.GatewayBot: + """Create and return a configured hikari GatewayBot.""" + if not config.DISCORD_TOKEN: + raise ValueError("DISCORD_TOKEN is not set") + return hikari.GatewayBot(token=config.DISCORD_TOKEN) + + +def create_lightbulb_client(bot: hikari.GatewayBot) -> lightbulb.Client: + """Create a lightbulb client from the bot.""" + return lightbulb.client_from_app(bot) + + +def get_guild_ids() -> list[int]: + """Get the guild IDs to register slash commands to. + + If TESTING_GUILD_ID is set, commands are guild-scoped (instant registration). + Otherwise, commands are global (may take up to an hour to propagate). + """ + if config.TESTING_GUILD_ID: + return [int(config.TESTING_GUILD_ID)] + return [] + + +def make_announce_fn(bot: hikari.GatewayBot) -> Callable[[str, str], Awaitable[None]]: + """Create an announce function that posts to a guild's designated channel. + + Returns an async function with signature: + async def announce(guild_id: str, message: str) -> None + + Note: game.py's AnnounceFn expects (message: str) -> None, so the caller + must partial-apply the guild_id. See commands.py for usage. + """ + async def announce(guild_id: str, message: str) -> None: + try: + # Look up the guild's designated channel via StackCoin API + users_resp = await stk.get_guild_channel(guild_id) + if users_resp is None: + logger.warning(f"No designated channel for guild {guild_id}") + return + + channel_id = int(users_resp) + channel = bot.cache.get_guild_channel(channel_id) + + if channel and isinstance(channel, hikari.TextableGuildChannel): + await channel.send(message) + logger.info(f"Announced to guild {guild_id} channel {channel_id}") + else: + logger.warning(f"Could not find textable channel {channel_id} for guild {guild_id}") + except Exception as e: + logger.error(f"Failed to announce to guild {guild_id}: {e}") + + return announce +``` + +**Step 2: Add `get_guild_channel` to `luckypot/stk.py`** + +This function was in the old `stk.py` (used `stackcoin_discord_guild`) but wasn't ported to the new httpx-based version. Add it: + +Append to `luckypot/stk.py`: + +```python +async def get_guild_channel(guild_id: str) -> str | None: + """Get the designated channel snowflake for a guild.""" + async with _client() as client: + resp = await client.get(f"/api/discord/guild/{guild_id}") + if resp.status_code != 200: + logger.debug(f"No guild info for {guild_id}: {resp.status_code}") + return None + data = resp.json() + return data.get("designated_channel_snowflake") +``` + +**Step 3: Commit** + +```bash +git add -A && git commit -m "feat: add bot lifecycle, announce_fn, and guild channel lookup" +``` + +--- + +### Task 5: Create `luckypot/discord/commands.py` — slash commands + +**Files:** +- Create: `luckypot/discord/commands.py` + +**Step 1: Create `commands.py`** + +```python +"""Slash commands for the LuckyPot Discord bot.""" +from functools import partial + +import hikari +import lightbulb +from loguru import logger + +from luckypot import config, db +from luckypot.game import enter_pot, end_pot_with_winner, POT_ENTRY_COST +from luckypot.discord import ui +from luckypot.discord.bot import get_guild_ids, make_announce_fn + + +def register_commands(client: lightbulb.Client, bot: hikari.GatewayBot) -> None: + """Register all slash commands on the lightbulb client.""" + guilds = get_guild_ids() + announce = make_announce_fn(bot) + + @client.register(guilds=guilds) + class EnterPot( + lightbulb.SlashCommand, + name="enter-pot", + description=f"Enter the daily lucky pot (costs {POT_ENTRY_COST} STK)", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + discord_id = str(ctx.user.id) + + try: + guild_announce = partial(announce, guild_id) + result = await enter_pot(discord_id, guild_id, announce_fn=guild_announce) + status = result.get("status", "error") + + if status == "pending": + container = ui.build_entry_pending( + request_id=result.get("request_id", 0), + amount=POT_ENTRY_COST, + ) + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + elif status == "instant_win": + container = ui.build_entry_instant_win() + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + elif status == "already_entered": + container = ui.build_entry_already_entered() + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + else: + message = result.get("message", "Something went wrong.") + container = ui.build_entry_error(message) + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + + except Exception as e: + logger.error(f"Error in /enter-pot for user {ctx.user.id}: {e}") + container = ui.build_entry_error(f"An unexpected error occurred: {e}") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + + @client.register(guilds=guilds) + class PotStatus( + lightbulb.SlashCommand, + name="pot-status", + description="Check the current pot status and participants", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + status = db.get_pot_status(conn, guild_id) + finally: + conn.close() + + container = ui.build_pot_status(status) + await ctx.respond(components=[container]) + + except Exception as e: + logger.error(f"Error in /pot-status for guild {ctx.guild_id}: {e}") + container = ui.build_entry_error("Error retrieving pot status. Please try again later.") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + + @client.register(guilds=guilds) + class PotHistory( + lightbulb.SlashCommand, + name="pot-history", + description="View recent pot winners", + ): + limit: int = lightbulb.integer("limit", "Number of recent pots to show", default=5, min_value=1, max_value=20) + + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + history = db.get_pot_history(conn, guild_id, limit=self.limit) + finally: + conn.close() + + container = ui.build_pot_history(history) + await ctx.respond(components=[container]) + + except Exception as e: + logger.error(f"Error in /pot-history for guild {ctx.guild_id}: {e}") + container = ui.build_entry_error("Error retrieving pot history.") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + + if config.DEBUG_MODE: + @client.register(guilds=guilds) + class ForceEndPot( + lightbulb.SlashCommand, + name="force-end-pot", + description="[DEBUG] Force end the current pot with a draw", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + status = db.get_pot_status(conn, guild_id) + finally: + conn.close() + + if not status.get("active"): + container = ui.build_entry_error("No active pot to end!") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + return + + if status["participants"] == 0: + container = ui.build_entry_error("Cannot end pot with no confirmed participants!") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + return + + guild_announce = partial(announce, guild_id) + won = await end_pot_with_winner(guild_id, win_type="DEBUG FORCE END", announce_fn=guild_announce) + + if won: + container = ui.build_entry_pending(0, 0) # placeholder + await ctx.respond("✅ Pot ended! Check the channel for the winner announcement.", flags=hikari.MessageFlag.EPHEMERAL) + else: + container = ui.build_entry_error("No confirmed participants found!") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) + + except Exception as e: + logger.error(f"Error in /force-end-pot for guild {ctx.guild_id}: {e}") + container = ui.build_entry_error(f"Error ending pot: {e}") + await ctx.respond(components=[container], flags=hikari.MessageFlag.EPHEMERAL) +``` + +**Step 2: Commit** + +```bash +git add -A && git commit -m "feat: add slash commands — enter-pot, pot-status, pot-history, force-end-pot" +``` + +--- + +### Task 6: Rewrite `lucky_pot.py` — wire everything together + +**Files:** +- Modify: `lucky_pot.py` + +**Step 1: Rewrite the entry point** + +```python +""" +LuckyPot Discord bot runner. + +Wires together three concurrent systems: +1. Hikari Discord bot (slash commands) +2. StackCoin WebSocket gateway (real-time event delivery) +3. Daily draw scheduler (asyncio sleep loop) +""" +import asyncio +from functools import partial + +import hikari +from loguru import logger + +from luckypot import config, db +from luckypot.gateway import StackCoinGateway +from luckypot.game import on_request_accepted, on_request_denied +from luckypot.discord.bot import create_bot, create_lightbulb_client, make_announce_fn +from luckypot.discord.commands import register_commands +from luckypot.discord.scheduler import run_daily_draw_loop + + +logger.add("lucky_pot.log", rotation="1 day", retention="7 days", level="INFO") + + +def main(): + """Initialize and run the LuckyPot bot.""" + logger.info("LuckyPot starting up...") + db.init_database() + + bot = create_bot() + client = create_lightbulb_client(bot) + register_commands(client, bot) + + # Subscribe lightbulb to handle its startup + bot.subscribe(hikari.StartingEvent, client.start) + + # Background tasks started once the bot is ready + background_tasks: list[asyncio.Task] = [] + + @bot.listen() + async def on_started(event: hikari.StartedEvent) -> None: + announce = make_announce_fn(bot) + + # --- StackCoin WebSocket gateway --- + gateway = StackCoinGateway(config.STACKCOIN_API_URL, config.STACKCOIN_API_TOKEN) + + async def handle_accepted(payload): + # Determine guild_id from the event data for announcements + guild_id = payload.get("data", {}).get("guild_id") + ann_fn = partial(announce, guild_id) if guild_id else None + await on_request_accepted(payload.get("data", {}), announce_fn=ann_fn) + + async def handle_denied(payload): + guild_id = payload.get("data", {}).get("guild_id") + ann_fn = partial(announce, guild_id) if guild_id else None + await on_request_denied(payload.get("data", {}), announce_fn=ann_fn) + + gateway.register_handler("request.accepted", handle_accepted) + gateway.register_handler("request.denied", handle_denied) + + gateway_task = asyncio.create_task(gateway.connect()) + background_tasks.append(gateway_task) + logger.info("StackCoin gateway started") + + # --- Daily draw scheduler --- + # For the daily draw, we don't have a single guild_id — the draw + # iterates over all active guilds internally. Pass None for now; + # game.daily_pot_draw handles multi-guild draws. Announcements for + # specific guilds happen inside process_pot_win which gets an announce_fn + # from end_pot_with_winner. We need to pass a guild-aware announce_fn. + # The simplest approach: pass None here and let the draw use the + # announce_fn pattern from game.py. We can enhance this later. + draw_task = asyncio.create_task(run_daily_draw_loop(announce_fn=None)) + background_tasks.append(draw_task) + logger.info("Daily draw scheduler started") + + @bot.listen() + async def on_stopping(event: hikari.StoppingEvent) -> None: + for task in background_tasks: + task.cancel() + logger.info("Background tasks cancelled") + + if config.DEBUG_MODE: + logger.info("DEBUG MODE ENABLED — /force-end-pot command available") + + bot.run() + + +if __name__ == "__main__": + main() +``` + +**Step 2: Commit** + +```bash +git add -A && git commit -m "feat: rewrite entry point to wire bot, gateway, and scheduler" +``` + +--- + +### Task 7: Update README and .gitignore + +**Files:** +- Modify: `README.md` +- Modify: `.gitignore` + +**Step 1: Update README.md** + +```markdown +# LuckyPot + +A lottery bot for [StackCoin](https://github.com/StackCoin/StackCoin). Players enter a pot by paying STK, and a daily draw selects a winner who takes the entire pot. There's also a 5% chance of an instant win on entry. + +## Architecture + +- `luckypot/` — Framework-agnostic core (game logic, database, StackCoin API client, WebSocket gateway) +- `luckypot/discord/` — Discord bot layer (hikari + lightbulb slash commands, rich UI, announcements) +- `lucky_pot.py` — Entry point that wires everything together + +## Setup + +```bash +# Install dependencies +uv sync + +# Copy and fill in environment variables +cp .env.dist .env + +# Run the bot +python lucky_pot.py +``` + +## Environment Variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `DISCORD_TOKEN` | Yes | | Discord bot token | +| `STACKCOIN_API_URL` | Yes | `http://localhost:4000` | StackCoin API base URL | +| `STACKCOIN_API_TOKEN` | Yes | | Bot API token from StackCoin | +| `STACKCOIN_WS_URL` | No | `ws://localhost:4000/ws` | StackCoin WebSocket URL | +| `LUCKYPOT_DB_PATH` | No | `luckypot.db` | SQLite database path | +| `TESTING_GUILD_ID` | No | | Restrict slash commands to one guild | +| `DEBUG_MODE` | No | `false` | Enable `/force-end-pot` command | +| `DAILY_DRAW_HOUR` | No | `0` | Daily draw hour (UTC) | +| `DAILY_DRAW_MINUTE` | No | `0` | Daily draw minute (UTC) | + +## Slash Commands + +- `/enter-pot` — Enter the daily lucky pot (costs 5 STK) +- `/pot-status` — Check the current pot status and participants +- `/pot-history` — View recent pot winners +- `/force-end-pot` — [DEBUG] Force end the current pot with a draw +``` + +**Step 2: Add `__pycache__/` to `.gitignore` if not already present** + +Check and append if needed: + +``` +__pycache__/ +*.pyc +``` + +**Step 3: Commit** + +```bash +git add -A && git commit -m "docs: update README and gitignore" +``` + +--- + +### Task 8: Smoke test — verify E2E tests still pass + +**Files:** None (verification only) + +**Step 1: Run the StackCoin E2E LuckyPot tests** + +```bash +cd /path/to/StackCoin/test/e2e +source .venv/bin/activate +python -m pytest test_luckypot.py -x -v --tb=short +``` + +Expected: All 16 luckypot tests pass. These tests import `from luckypot import db, game, stk` and never touch the `luckypot.discord` subpackage. + +**Step 2: Verify the package builds** + +```bash +cd /path/to/LuckyPot +uv sync +python -c "from luckypot.discord.bot import create_bot; print('discord layer imports OK')" +python -c "from luckypot.game import enter_pot; print('core imports OK')" +``` + +Both should print OK without errors. + +**Step 3: Verify the bot starts (if Discord token is available)** + +```bash +python lucky_pot.py +``` + +If `DISCORD_TOKEN` is set, the bot should connect to Discord and log "LuckyPot starting up..." followed by gateway connection messages. Ctrl+C to stop. + +If `DISCORD_TOKEN` is not set, it should raise `ValueError: DISCORD_TOKEN is not set`. diff --git a/justfile b/justfile index 7d24720..aaace8e 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,2 @@ install_local_stackcoin_python: uv pip install -e "stackcoin @ ../stackcoin-python/stackcoin" - diff --git a/lucky_pot.py b/lucky_pot.py index 6bdea84..8673468 100644 --- a/lucky_pot.py +++ b/lucky_pot.py @@ -1,451 +1,93 @@ -import random import asyncio -import sqlite3 -from datetime import datetime, timedelta, timezone -from typing_extensions import TypedDict - -from hikari.impl.special_endpoints import ContainerComponentBuilder import hikari -import lightbulb -import schedule from loguru import logger -import db -import stk -import config - - -POT_ENTRY_COST = 5 -CHECK_INTERVAL_SECONDS = 30 -DAILY_DRAW_CHANCE = 0.6 -RANDOM_WIN_CHANCE = 0.05 - -MESSAGES = { - "not_registered": "❌ You are not registered with StackCoin! Please run `/dole` first.", - "payment_request_failed": "❌ Failed to create payment request.", - "already_entered": "❌ You have already entered this pot! You can only enter once per pot.", - "no_active_pot": "🎲 No active pot! Use `/enter-pot` to start one.", - "pot_status_error": "❌ Error retrieving pot status. Please try again later.", - "debug_no_active_pot": "❌ No active pot to end!", - "debug_no_participants": "❌ Cannot end pot with no participants!", - "debug_no_confirmed_participants": "❌ No confirmed participants found!", -} - - -def announce_winner( - guild_id: str, winner_id: str, winning_amount: int, win_type: str = "DAILY DRAW" -): - """Create winner announcement message""" - return ( - f"🎉 **{win_type} WINNER!** 🎉\n\n" - f"<@{winner_id}> has won the pot of **{winning_amount} STK**!\n" - f"Congratulations! 🎊\n\n" - f"A new pot has started - use `/enter-pot` to join!" - ) - - -def announce_daily_draw_no_winner(guild_id: str, pot_amount: int): - """Create daily draw no winner announcement""" - return ( - f"🎲 Daily draw occurred, but the pot continues! No winner this time.\n" - f"Current pot: **{pot_amount} STK**\n" - f"Use `/enter-pot` to increase your chances!" - ) - - -def create_pot_status_container(status): - """Create container component for pot status display""" - - container = ContainerComponentBuilder(accent_color=hikari.Color(0x00FF00)) - container.add_text_display("🎰 Lucky Pot Status") - container.add_text_display(f"💰 Total Pot: {status['total_amount']} STK") - - container.add_text_display(f"👥 Participants: {status['participant_count']}") - - if status.get("participants"): - participant_list = [] - for p in status["participants"][:10]: - participant_list.append(f"<@{p['discord_id']}>") - - if participant_list: - participant_text = "\n".join(participant_list) - container.add_text_display(f"🏆 Participants\n{participant_text}") - - next_draw = datetime.now(timezone.utc).replace( - hour=0, minute=0, second=0, microsecond=0 - ) + timedelta(days=1) - - container.add_text_display( - f"⏰ Next Daily Draw: " - ) - - container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) - - container.add_text_display(f"Pot ID: {status['pot_id']}") - - return container - - -def get_instant_win_message(username: str, pot_total: int): - """Get instant win message""" - return ( - f"🎉 **INSTANT WIN!** {username}, you've won the entire pot of {pot_total} STK!" - ) - -def get_entry_confirmation_message(username: str): - """Get entry confirmation message""" - return f"🎲 {username}, accept the {POT_ENTRY_COST} STK payment request from StackCoin via DMs, and you're in the pot!" - - -def get_debug_success_message(winner_id: str, winning_amount: int): - """Get debug success message""" - return f"✅ Pot ended! Winner: <@{winner_id}> won {winning_amount} STK" - - -class WinnerInfo(TypedDict): - winner_id: str - winning_amount: int - participant_count: int +from luckypot import db +from luckypot.config import settings +import stackcoin +from luckypot.game import on_request_accepted, on_request_denied +from luckypot.discord.bot import ( + create_bot, + create_lightbulb_client, + make_announce_fn, + make_edit_announce_fn, +) +from luckypot.discord.commands import register_commands +from luckypot.discord.scheduler import run_daily_draw_loop logger.add("lucky_pot.log", rotation="1 day", retention="7 days", level="INFO") -logger.info("Starting LuckyPot Discord Bot") - -if not config.DISCORD_TOKEN: - raise ValueError("LUCKY_POT_DISCORD_TOKEN is not set") - - -async def send_winnings_to_user(winner_discord_id: str, amount: int) -> bool: - """Send STK winnings to the winner""" - return await stk.send_stk(winner_discord_id, amount, "Lucky Pot Winnings") - - -async def announce_to_guild(guild_id: str, message: str) -> None: - """Send announcement to guild's designated channel""" - channel_snowflake = await stk.get_guild_channel(guild_id) - - if channel_snowflake: - channel_id = int(channel_snowflake) - channel = bot.cache.get_guild_channel(channel_id) - - if channel and isinstance(channel, hikari.TextableGuildChannel): - await channel.send(message) - logger.info(f"Announced to guild {guild_id}: {message}") - else: - logger.warning(f"Could not find channel {channel_id} in guild {guild_id}") - else: - logger.warning(f"No designated channel found for guild {guild_id}") - - -async def process_stackcoin_requests(): - """Background task to process StackCoin requests""" - with db.get_connection() as conn: - unconfirmed_entries = db.get_unconfirmed_entries(conn) - - accepted_requests = await stk.get_accepted_requests() - - for entry in unconfirmed_entries: - for request in accepted_requests: - if str(request.id) == entry["stackcoin_request_id"]: - if entry["status"] == "instant_win": - with db.get_transaction() as conn: - db.confirm_entry(conn, entry["entry_id"]) - pot_status = db.get_pot_status(conn, entry["pot_guild_id"]) - if pot_status is not None: - winning_amount = pot_status["total_amount"] - await process_pot_win( - conn, - entry["pot_guild_id"], - entry["discord_id"], - winning_amount, - "INSTANT WIN", - ) - else: - with db.get_transaction() as conn: - db.confirm_entry(conn, entry["entry_id"]) - - logger.info( - f"Confirmed entry {entry['entry_id']} for request {request.id}" - ) - break - - with db.get_connection() as conn: - expired_entries = db.get_expired_entries(conn) - - for entry in expired_entries: - if await stk.deny_request(entry["stackcoin_request_id"]): - with db.get_transaction() as conn: - db.deny_entry(conn, entry["entry_id"]) - logger.info(f"Denied expired entry {entry['entry_id']}") - - -def select_random_winner(participants: list[db.Participant]) -> str: - """Select a random winner from participants""" - participant_ids = [p["discord_id"] for p in participants] - return random.choice(participant_ids) - - -async def process_pot_win( - conn: sqlite3.Connection, - guild_id: str, - winner_id: str, - winning_amount: int, - win_type: str = "DAILY DRAW", -) -> bool: - """Process a pot win: send winnings and announce""" - if await send_winnings_to_user(winner_id, winning_amount): - db.win_pot(conn, guild_id, winner_id, winning_amount) - - await announce_to_guild( - guild_id, announce_winner(guild_id, winner_id, winning_amount, win_type) - ) - - logger.info( - f"{win_type} winner in guild {guild_id}: {winner_id} won {winning_amount} STK" - ) - return True - else: - logger.error(f"Failed to send winnings to {winner_id} in guild {guild_id}") - return False - - -async def end_pot_with_winner( - guild_id: str, win_type: str = "DAILY DRAW" -) -> WinnerInfo | None: - """End a pot by selecting and paying a winner. Returns winner info or None if failed.""" - with db.get_transaction() as conn: - pot_status = db.get_pot_status(conn, guild_id) - - if pot_status is None or pot_status["participant_count"] == 0: - return None - - participants = db.get_active_pot_participants(conn, guild_id) - if not participants: - return None - - winner_id = select_random_winner(participants) - winning_amount = pot_status["total_amount"] - - if await process_pot_win(conn, guild_id, winner_id, winning_amount, win_type): - return WinnerInfo( - winner_id=winner_id, - winning_amount=winning_amount, - participant_count=pot_status["participant_count"], - ) - - return None - - -async def daily_pot_draw(): - """Daily pot draw at UTC 0 with 40% win chance""" - with db.get_connection() as conn: - all_guilds = db.get_all_active_guilds(conn) - - for guild_id in all_guilds: - with db.get_connection() as conn: - pot_status = db.get_pot_status(conn, guild_id) - - if pot_status is None or pot_status["participant_count"] == 0: - continue - - if random.random() < DAILY_DRAW_CHANCE: - winner_info = await end_pot_with_winner(guild_id, "DAILY DRAW") - if not winner_info: - await announce_to_guild( - guild_id, - announce_daily_draw_no_winner( - guild_id, pot_status["total_amount"] - ), - ) - else: - await announce_to_guild( - guild_id, - announce_daily_draw_no_winner(guild_id, pot_status["total_amount"]), - ) - - -async def background_tasks(): - """Run background tasks periodically""" - while True: - try: - await process_stackcoin_requests() - - schedule.run_pending() - - except Exception as e: - logger.error(f"Error in background tasks: {e}") - - await asyncio.sleep(CHECK_INTERVAL_SECONDS) - - -schedule.every().day.at("00:00").do(lambda: asyncio.create_task(daily_pot_draw())) +logger.info("LuckyPot starting up...") db.init_database() -bot = hikari.GatewayBot(token=config.DISCORD_TOKEN) -lightbulb_client = lightbulb.client_from_app(bot) +bot = create_bot() +client = create_lightbulb_client(bot) +register_commands(client, bot) -bot.subscribe(hikari.StartingEvent, lightbulb_client.start) +bot.subscribe(hikari.StartingEvent, client.start) - -@bot.listen() -async def on_starting(event: hikari.StartingEvent) -> None: - """Start background tasks when bot starts""" - try: - asyncio.create_task(background_tasks()) - except Exception as e: - logger.error(f"Critical startup error: {e}") - exit(0) +background_tasks: list[asyncio.Task] = [] -guilds = [int(config.TESTING_GUILD_ID)] if config.TESTING_GUILD_ID else [] - - -@lightbulb_client.register(guilds=guilds) -class EnterPot( - lightbulb.SlashCommand, - name="enter-pot", - description=f"Enter the daily lucky pot (costs {POT_ENTRY_COST} STK)", -): - @lightbulb.invoke - async def invoke(self, ctx: lightbulb.Context) -> None: - try: - guild_id = str(ctx.guild_id) - discord_id = str(ctx.user.id) - - user = await stk.get_user_by_discord_id(discord_id) - - if not user: - await ctx.respond(MESSAGES["not_registered"]) - return - - if not isinstance(user.id, int): - raise Exception("User ID is not an integer") - - request_id = await stk.create_payment_request( - user.id, POT_ENTRY_COST, "Lucky Pot Entry" - ) - - if not request_id: - await ctx.respond(MESSAGES["payment_request_failed"]) - return - - instant_win = random.random() < RANDOM_WIN_CHANCE - - with db.get_transaction() as conn: - db.get_or_create_user(conn, discord_id, guild_id) - - current_pot = db.get_current_pot(conn, guild_id) - if not current_pot: - pot_id = db.create_new_pot(conn, guild_id) - else: - pot_id = current_pot["pot_id"] - - if not db.can_user_enter_pot(conn, discord_id, guild_id, pot_id): - await ctx.respond(MESSAGES["already_entered"]) - return - - entry_id = db.create_pot_entry( - conn, - pot_id, - discord_id, - guild_id, - request_id, - instant_win, - ) - - if instant_win: - current_status = db.get_pot_status(conn, guild_id) - if current_status is None: - raise Exception("No active pot, but we just created one?") - pot_total = current_status.get("total_amount", 0) + POT_ENTRY_COST - else: - pot_total = None - - logger.debug(f"Pot Entry ID: {entry_id}") - - if instant_win: - await ctx.respond( - get_instant_win_message(user.username, pot_total or 0) - ) - else: - await ctx.respond(get_entry_confirmation_message(user.username)) - - except Exception as e: - logger.error(f"Error in EnterPot command for user {ctx.user.id}: {e}") - await ctx.respond(f"❌ Error creating pot entry: {str(e)}") +@bot.listen() +async def on_started(_event: hikari.StartedEvent) -> None: + announce = make_announce_fn(bot) + edit_announce = make_edit_announce_fn(bot) + conn = db.get_connection() + try: + last_event_id = db.get_last_event_id(conn) + finally: + conn.close() + logger.info(f"Resuming gateway from event {last_event_id}") -@lightbulb_client.register(guilds=guilds) -class PotStatus( - lightbulb.SlashCommand, - name="pot-status", - description="Check the current pot status and participants", -): - @lightbulb.invoke - async def invoke(self, ctx: lightbulb.Context) -> None: + def persist_event_id(event_id: int) -> None: + c = db.get_connection() try: - guild_id = str(ctx.guild_id) - - with db.get_connection() as conn: - status = db.get_pot_status(conn, guild_id) - - if status is None: - await ctx.respond(MESSAGES["no_active_pot"]) - return - - container_builder = create_pot_status_container(status) - await ctx.respond(components=[container_builder]) - - except Exception as e: - logger.error(f"Error in PotStatus command for guild {ctx.guild_id}: {e}") - await ctx.respond(MESSAGES["pot_status_error"]) - + db.set_last_event_id(c, event_id) + finally: + c.close() + + gateway = stackcoin.Gateway( + ws_url=settings.stackcoin_ws_url, + token=settings.stackcoin_api_token, + last_event_id=last_event_id, + on_event_id=persist_event_id, + ) -if config.DEBUG_MODE: + async def handle_accepted(event: stackcoin.RequestAcceptedEvent): + await on_request_accepted(event.data, announce=announce) - @lightbulb_client.register(guilds=guilds) - class ForceEndPot( - lightbulb.SlashCommand, - name="force-end-pot", - description="[DEBUG] Force end the current pot with a draw", - ): - @lightbulb.invoke - async def invoke(self, ctx: lightbulb.Context) -> None: - try: - guild_id = str(ctx.guild_id) + async def handle_denied(event: stackcoin.RequestDeniedEvent): + await on_request_denied(event.data, announce=announce) - with db.get_connection() as conn: - pot_status = db.get_pot_status(conn, guild_id) + gateway.register_handler("request.accepted", handle_accepted) + gateway.register_handler("request.denied", handle_denied) - if pot_status is None: - await ctx.respond(MESSAGES["debug_no_active_pot"]) - return + gateway_task = asyncio.create_task(gateway.connect()) + background_tasks.append(gateway_task) + logger.info("StackCoin gateway started") - if pot_status["participant_count"] == 0: - await ctx.respond(MESSAGES["debug_no_participants"]) - return + draw_task = asyncio.create_task( + run_daily_draw_loop( + announce=announce, + edit_announce=edit_announce, + ) + ) + background_tasks.append(draw_task) + logger.info("Daily draw scheduler started") - winner_info = await end_pot_with_winner(guild_id, "DEBUG FORCE END") - if winner_info: - await ctx.respond( - get_debug_success_message( - winner_info["winner_id"], winner_info["winning_amount"] - ) - ) - else: - await ctx.respond(MESSAGES["debug_no_confirmed_participants"]) +@bot.listen() +async def on_stopping(_event: hikari.StoppingEvent) -> None: + for task in background_tasks: + task.cancel() + logger.info("Background tasks cancelled") - except Exception as e: - logger.error( - f"Error in ForceEndPot command for guild {ctx.guild_id}: {e}" - ) - await ctx.respond(f"❌ Error ending pot: {str(e)}") +if settings.debug_mode: + logger.info("DEBUG MODE ENABLED — /force-end-pot command available") -if __name__ == "__main__": - if config.DEBUG_MODE: - logger.info("DEBUG MODE ENABLED - /force-end-pot command available") - bot.run() +bot.run() diff --git a/luckypot/__init__.py b/luckypot/__init__.py new file mode 100644 index 0000000..8526e19 --- /dev/null +++ b/luckypot/__init__.py @@ -0,0 +1 @@ +"""LuckyPot - A lottery bot for StackCoin.""" diff --git a/luckypot/config.py b/luckypot/config.py new file mode 100644 index 0000000..b8fe739 --- /dev/null +++ b/luckypot/config.py @@ -0,0 +1,28 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="LUCKYPOT_", + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + ) + + discord_token: str = "" + stackcoin_api_url: str = "http://localhost:4000" + stackcoin_api_token: str = "" + stackcoin_ws_url: str = "ws://localhost:4000/ws" + db_path: str = "luckypot.db" + + testing_guild_id: str = "" + debug_mode: bool = False + + daily_draw_hour: int = 0 + daily_draw_minute: int = 0 + draw_interval_minutes: int = ( + 0 # When >0, overrides daily schedule with a repeating interval + ) + + +settings = Settings() diff --git a/luckypot/db.py b/luckypot/db.py new file mode 100644 index 0000000..b2fc460 --- /dev/null +++ b/luckypot/db.py @@ -0,0 +1,257 @@ +import sqlite3 +from loguru import logger +from luckypot.config import settings + + +def get_connection() -> sqlite3.Connection: + conn = sqlite3.connect(settings.db_path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + return conn + + +def init_database(): + conn = get_connection() + conn.executescript(""" + CREATE TABLE IF NOT EXISTS pots ( + pot_id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ended_at TIMESTAMP, + winner_discord_id TEXT, + winning_amount INTEGER, + win_type TEXT + ); + + CREATE TABLE IF NOT EXISTS pot_entries ( + entry_id INTEGER PRIMARY KEY AUTOINCREMENT, + pot_id INTEGER NOT NULL, + discord_id TEXT NOT NULL, + amount INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + stackcoin_request_id TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (pot_id) REFERENCES pots(pot_id) + ); + + CREATE TABLE IF NOT EXISTS gateway_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_pots_guild_active + ON pots(guild_id, is_active); + CREATE INDEX IF NOT EXISTS idx_pot_entries_pot_id + ON pot_entries(pot_id); + CREATE INDEX IF NOT EXISTS idx_pot_entries_request_id + ON pot_entries(stackcoin_request_id); + """) + conn.commit() + conn.close() + logger.info("Database initialized") + + +def get_active_pot(conn, guild_id: str) -> dict | None: + """Get the active pot for a guild, or None if there isn't one.""" + cursor = conn.execute( + "SELECT * FROM pots WHERE guild_id = ? AND is_active = TRUE", + (guild_id,), + ) + row = cursor.fetchone() + return dict(row) if row else None + + +def create_pot(conn, guild_id: str) -> dict: + """Create a new active pot for a guild.""" + cursor = conn.execute( + "INSERT INTO pots (guild_id) VALUES (?)", + (guild_id,), + ) + conn.commit() + return {"pot_id": cursor.lastrowid, "guild_id": guild_id, "is_active": True} + + +def ensure_active_pot(conn, guild_id: str) -> dict: + """Return the active pot for a guild, creating one if necessary.""" + pot = get_active_pot(conn, guild_id) + if pot is None: + pot = create_pot(conn, guild_id) + return pot + + +def end_pot( + conn, pot_id: int, winner_discord_id: str | None, winning_amount: int, win_type: str +): + """Mark a pot as ended with a winner.""" + conn.execute( + """UPDATE pots + SET is_active = FALSE, + ended_at = CURRENT_TIMESTAMP, + winner_discord_id = ?, + winning_amount = ?, + win_type = ? + WHERE pot_id = ?""", + (winner_discord_id, winning_amount, win_type, pot_id), + ) + conn.commit() + + +def add_entry( + conn, + pot_id: int, + discord_id: str, + amount: int, + stackcoin_request_id: str | None = None, + status: str = "pending", +) -> int: + """Add an entry to a pot. Returns the entry_id.""" + cursor = conn.execute( + """INSERT INTO pot_entries (pot_id, discord_id, amount, status, stackcoin_request_id) + VALUES (?, ?, ?, ?, ?)""", + (pot_id, discord_id, amount, status, stackcoin_request_id), + ) + conn.commit() + return cursor.lastrowid + + +def get_entry_by_id(conn, entry_id: int) -> dict | None: + """Get a pot entry by its ID.""" + cursor = conn.execute("SELECT * FROM pot_entries WHERE entry_id = ?", (entry_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + +def get_entry_by_request_id(conn, request_id: str) -> dict | None: + """Get a pot entry by its StackCoin request ID, including pot guild_id.""" + cursor = conn.execute( + """SELECT pe.*, p.guild_id AS pot_guild_id + FROM pot_entries pe + JOIN pots p ON pe.pot_id = p.pot_id + WHERE pe.stackcoin_request_id = ?""", + (request_id,), + ) + row = cursor.fetchone() + return dict(row) if row else None + + +def confirm_entry(conn, entry_id: int): + """Mark an entry as confirmed (payment received).""" + conn.execute( + "UPDATE pot_entries SET status = 'confirmed' WHERE entry_id = ?", + (entry_id,), + ) + conn.commit() + + +def deny_entry(conn, entry_id: int): + """Mark an entry as denied (payment rejected).""" + conn.execute( + "UPDATE pot_entries SET status = 'denied' WHERE entry_id = ?", + (entry_id,), + ) + conn.commit() + + +def mark_entry_instant_win(conn, entry_id: int): + """Mark an entry as an instant win (pending payment to winner).""" + conn.execute( + "UPDATE pot_entries SET status = 'instant_win' WHERE entry_id = ?", + (entry_id,), + ) + conn.commit() + + +def get_confirmed_entries(conn, pot_id: int) -> list[dict]: + """Get all confirmed entries for a pot.""" + cursor = conn.execute( + "SELECT * FROM pot_entries WHERE pot_id = ? AND status = 'confirmed'", + (pot_id,), + ) + return [dict(row) for row in cursor.fetchall()] + + +def get_pot_participants(conn, pot_id: int) -> list[dict]: + """Get all confirmed and instant_win entries for a pot (active participants).""" + cursor = conn.execute( + "SELECT * FROM pot_entries WHERE pot_id = ? AND status IN ('confirmed', 'instant_win')", + (pot_id,), + ) + return [dict(row) for row in cursor.fetchall()] + + +def get_pot_status(conn, guild_id: str) -> dict: + """Get the current pot status for a guild.""" + pot = get_active_pot(conn, guild_id) + if pot is None: + return {"active": False, "participants": 0, "total_amount": 0} + + cursor = conn.execute( + """SELECT COUNT(*) as count, COALESCE(SUM(amount), 0) as total + FROM pot_entries + WHERE pot_id = ? AND status IN ('confirmed', 'instant_win')""", + (pot["pot_id"],), + ) + row = cursor.fetchone() + return { + "active": True, + "pot_id": pot["pot_id"], + "participants": row["count"], + "total_amount": row["total"], + } + + +def has_user_entered(conn, pot_id: int, discord_id: str) -> bool: + """Check if a user has already entered the active pot.""" + cursor = conn.execute( + """SELECT COUNT(*) as count FROM pot_entries + WHERE pot_id = ? AND discord_id = ? AND status IN ('pending', 'confirmed', 'instant_win')""", + (pot_id, discord_id), + ) + return cursor.fetchone()["count"] > 0 + + +def has_pending_instant_wins(conn, guild_id: str) -> bool: + """Check if there are any pending instant win entries for a guild's active pot.""" + cursor = conn.execute( + """SELECT COUNT(*) as count FROM pot_entries pe + JOIN pots p ON pe.pot_id = p.pot_id + WHERE p.guild_id = ? AND p.is_active = TRUE AND pe.status = 'instant_win'""", + (guild_id,), + ) + return cursor.fetchone()["count"] > 0 + + +def get_all_active_guilds(conn) -> list[str]: + """Get all guild_ids that have an active pot.""" + cursor = conn.execute("SELECT DISTINCT guild_id FROM pots WHERE is_active = TRUE") + return [row["guild_id"] for row in cursor.fetchall()] + + +def get_pot_history(conn, guild_id: str, limit: int = 10) -> list[dict]: + """Get recent pot history for a guild.""" + cursor = conn.execute( + """SELECT * FROM pots + WHERE guild_id = ? AND is_active = FALSE + ORDER BY ended_at DESC LIMIT ?""", + (guild_id, limit), + ) + return [dict(row) for row in cursor.fetchall()] + + +def get_last_event_id(conn) -> int: + """Get the last processed gateway event ID, or 0 if none.""" + cursor = conn.execute("SELECT value FROM gateway_state WHERE key = 'last_event_id'") + row = cursor.fetchone() + return int(row["value"]) if row else 0 + + +def set_last_event_id(conn, event_id: int) -> None: + """Persist the last processed gateway event ID.""" + conn.execute( + "INSERT INTO gateway_state (key, value) VALUES ('last_event_id', ?) " + "ON CONFLICT(key) DO UPDATE SET value = excluded.value", + (str(event_id),), + ) + conn.commit() diff --git a/luckypot/discord/__init__.py b/luckypot/discord/__init__.py new file mode 100644 index 0000000..17eef57 --- /dev/null +++ b/luckypot/discord/__init__.py @@ -0,0 +1 @@ +"""Discord bot layer for LuckyPot.""" diff --git a/luckypot/discord/bot.py b/luckypot/discord/bot.py new file mode 100644 index 0000000..c7b8b75 --- /dev/null +++ b/luckypot/discord/bot.py @@ -0,0 +1,76 @@ +import hikari +import lightbulb +from loguru import logger + +from luckypot import stk +from luckypot.config import settings + + +def create_bot() -> hikari.GatewayBot: + if not settings.discord_token: + raise ValueError("LUCKYPOT_DISCORD_TOKEN is not set") + return hikari.GatewayBot(token=settings.discord_token) + + +def create_lightbulb_client(bot: hikari.GatewayBot) -> lightbulb.Client: + return lightbulb.client_from_app(bot) + + +def get_guild_ids() -> list[int]: + """Get the guild IDs to register slash commands to.""" + if settings.testing_guild_id: + return [int(settings.testing_guild_id)] + return [] + + +def _get_guild_channel(bot: hikari.GatewayBot, guild_id: str): + """Resolve the designated textable channel for a guild. Returns (channel, channel_id) or (None, None).""" + + async def resolve(): + channel_snowflake = await stk.get_guild_channel(guild_id) + if channel_snowflake is None: + logger.warning(f"No designated channel for guild {guild_id}") + return None, None + + cid = int(channel_snowflake) + channel = bot.cache.get_guild_channel(cid) + + if channel and isinstance(channel, hikari.TextableGuildChannel): + return channel, cid + + logger.warning(f"Could not find textable channel {cid} for guild {guild_id}") + return None, None + + return resolve() + + +def make_announce_fn(bot: hikari.GatewayBot): + async def announce(guild_id: str, message: str) -> hikari.Message | None: + try: + channel, channel_id = await _get_guild_channel(bot, guild_id) + if channel is None: + return None + + msg = await channel.send(message) + logger.info(f"Announced to guild {guild_id} channel {channel_id}") + return msg + except Exception as e: + logger.error(f"Failed to announce to guild {guild_id}: {e}") + return None + + return announce + + +def make_edit_announce_fn(bot: hikari.GatewayBot): + async def edit_announce( + guild_id: str, message: hikari.Message, new_content: str + ) -> hikari.Message | None: + try: + msg = await message.edit(new_content) + logger.info(f"Edited announcement in guild {guild_id}") + return msg + except Exception as e: + logger.error(f"Failed to edit announcement in guild {guild_id}: {e}") + return None + + return edit_announce diff --git a/luckypot/discord/commands.py b/luckypot/discord/commands.py new file mode 100644 index 0000000..4d4f375 --- /dev/null +++ b/luckypot/discord/commands.py @@ -0,0 +1,197 @@ +from functools import partial + +import hikari +import lightbulb +from loguru import logger + +from luckypot import db +from luckypot.config import settings +from luckypot.game import enter_pot, end_pot_with_winner, POT_ENTRY_COST +from luckypot.discord import ui +from luckypot.discord.bot import get_guild_ids, make_announce_fn, make_edit_announce_fn + + +def register_commands(client: lightbulb.Client, bot: hikari.GatewayBot) -> None: + guilds = get_guild_ids() + announce = make_announce_fn(bot) + edit_announce = make_edit_announce_fn(bot) + + @client.register(guilds=guilds) + class EnterPot( + lightbulb.SlashCommand, + name="enter-pot", + description=f"Enter the daily lucky pot (costs {POT_ENTRY_COST} STK)", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + discord_id = str(ctx.user.id) + + try: + guild_announce = partial(announce, guild_id) + result = await enter_pot( + discord_id, guild_id, announce_fn=guild_announce + ) + status = result.get("status", "error") + + if status == "pending": + container = ui.build_entry_pending( + request_id=result.get("request_id", 0), + amount=POT_ENTRY_COST, + ) + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + elif status == "instant_win": + container = ui.build_entry_instant_win() + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + elif status == "already_entered": + container = ui.build_entry_already_entered() + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + else: + message = result.get("message", "Something went wrong.") + container = ui.build_entry_error(message) + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + + except Exception as e: + logger.error(f"Error in /enter-pot for user {ctx.user.id}: {e}") + container = ui.build_entry_error(f"An unexpected error occurred: {e}") + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + + @client.register(guilds=guilds) + class PotStatus( + lightbulb.SlashCommand, + name="pot-status", + description="Check the current pot status and participants", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + status = db.get_pot_status(conn, guild_id) + finally: + conn.close() + + container = ui.build_pot_status(status) + await ctx.respond(components=[container]) + + except Exception as e: + logger.error(f"Error in /pot-status for guild {ctx.guild_id}: {e}") + container = ui.build_entry_error( + "Error retrieving pot status. Please try again later." + ) + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + + @client.register(guilds=guilds) + class PotHistory( + lightbulb.SlashCommand, + name="pot-history", + description="View recent pot winners", + ): + limit: int = lightbulb.integer( + "limit", + "Number of recent pots to show", + default=5, + min_value=1, + max_value=20, + ) + + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + history = db.get_pot_history(conn, guild_id, limit=self.limit) + finally: + conn.close() + + container = ui.build_pot_history(history) + await ctx.respond(components=[container]) + + except Exception as e: + logger.error(f"Error in /pot-history for guild {ctx.guild_id}: {e}") + container = ui.build_entry_error("Error retrieving pot history.") + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + + if settings.debug_mode: + + @client.register(guilds=guilds) + class ForceEndPot( + lightbulb.SlashCommand, + name="force-end-pot", + description="[DEBUG] Force end the current pot with a draw", + ): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + guild_id = str(ctx.guild_id) + + try: + conn = db.get_connection() + try: + status = db.get_pot_status(conn, guild_id) + finally: + conn.close() + + if not status.get("active"): + container = ui.build_entry_error("No active pot to end!") + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + return + + if status["participants"] == 0: + container = ui.build_entry_error( + "Cannot end pot with no confirmed participants!" + ) + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + return + + guild_announce = partial(announce, guild_id) + guild_edit = partial(edit_announce, guild_id) + won = await end_pot_with_winner( + guild_id, + win_type="DEBUG FORCE END", + announce_fn=guild_announce, + edit_announce_fn=guild_edit, + ) + + if won: + await ctx.respond( + "✅ Pot ended! Check the channel for the winner announcement.", + flags=hikari.MessageFlag.EPHEMERAL, + ) + else: + container = ui.build_entry_error( + "No confirmed participants found!" + ) + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) + + except Exception as e: + logger.error( + f"Error in /force-end-pot for guild {ctx.guild_id}: {e}" + ) + container = ui.build_entry_error(f"Error ending pot: {e}") + await ctx.respond( + components=[container], flags=hikari.MessageFlag.EPHEMERAL + ) diff --git a/luckypot/discord/scheduler.py b/luckypot/discord/scheduler.py new file mode 100644 index 0000000..850e148 --- /dev/null +++ b/luckypot/discord/scheduler.py @@ -0,0 +1,57 @@ +import asyncio +from datetime import datetime, timedelta, timezone + +from loguru import logger + +from luckypot.config import settings +from luckypot.game import daily_pot_draw, RawAnnounceFn, RawEditAnnounceFn + + +def next_draw_time() -> datetime: + """Calculate the next draw time based on current config. + + In interval mode, returns now + interval. Otherwise returns the next + occurrence of the configured daily draw time. + """ + now = datetime.now(timezone.utc) + if settings.draw_interval_minutes > 0: + return now + timedelta(minutes=settings.draw_interval_minutes) + + next_draw = now.replace( + hour=settings.daily_draw_hour, + minute=settings.daily_draw_minute, + second=0, + microsecond=0, + ) + if next_draw <= now: + next_draw += timedelta(days=1) + return next_draw + + +async def run_daily_draw_loop( + announce: RawAnnounceFn = None, + edit_announce: RawEditAnnounceFn = None, +): + """Run the pot draw on a schedule. + + When ``draw_interval_minutes`` is set (>0), runs on a repeating interval + (useful for testing). Otherwise, runs once per day at the configured + ``daily_draw_hour:daily_draw_minute`` UTC time. + + ``announce`` and ``edit_announce`` are the raw bot functions that + take ``guild_id`` as their first argument. ``daily_pot_draw`` will + create per-guild partials internally. + """ + while True: + next_draw = next_draw_time() + now = datetime.now(timezone.utc) + sleep_seconds = (next_draw - now).total_seconds() + logger.info(f"Next draw at {next_draw.isoformat()} (in {sleep_seconds:.0f}s)") + + await asyncio.sleep(sleep_seconds) + + logger.info("Running daily pot draw...") + try: + await daily_pot_draw(announce=announce, edit_announce=edit_announce) + except Exception as e: + logger.error(f"Daily draw failed: {e}") diff --git a/luckypot/discord/ui.py b/luckypot/discord/ui.py new file mode 100644 index 0000000..cffff21 --- /dev/null +++ b/luckypot/discord/ui.py @@ -0,0 +1,117 @@ +import hikari +from hikari.impl.special_endpoints import ContainerComponentBuilder + +from luckypot.discord.scheduler import next_draw_time + +BRAND_COLOR = hikari.Color(0x7C3AED) + + +def build_entry_pending(request_id: int, amount: int) -> ContainerComponentBuilder: + """Build response for a successful pot entry (pending payment).""" + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("🎲 Pot Entry Submitted!") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display( + f"Accept the **{amount} STK** payment request from StackCoin via DMs to confirm your spot." + ) + return container + + +def build_entry_instant_win() -> ContainerComponentBuilder: + """Build response for an instant win roll (still needs payment).""" + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("🎉 INSTANT WIN!") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display( + "You rolled an **instant win**! Accept the payment request to claim the entire pot!" + ) + return container + + +def build_entry_already_entered() -> ContainerComponentBuilder: + """Build response when user has already entered the pot.""" + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("❌ Already Entered") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display( + "You have already entered this pot! You can only enter once per pot." + ) + return container + + +def build_entry_error(message: str) -> ContainerComponentBuilder: + """Build response for a pot entry error.""" + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("❌ Error") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(message) + return container + + +def build_pot_status(status: dict) -> ContainerComponentBuilder: + """Build the pot status display.""" + if not status.get("active"): + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("🎲 No Active Pot") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display("Use `/enter-pot` to start one!") + return container + + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("🎰 Lucky Pot Status") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(f"💰 Total Pot: **{status['total_amount']} STK**") + container.add_text_display(f"👥 Participants: **{status['participants']}**") + + next_draw = next_draw_time() + container.add_text_display(f"⏰ Next Draw: ") + + if "pot_id" in status: + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + container.add_text_display(f"Pot ID: {status['pot_id']}") + + return container + + +def build_pot_history(history: list[dict]) -> ContainerComponentBuilder: + """Build the pot history display.""" + container = ContainerComponentBuilder(accent_color=BRAND_COLOR) + container.add_text_display("📜 Pot History") + container.add_separator(divider=True, spacing=hikari.SpacingType.SMALL) + + if not history: + container.add_text_display("No completed pots yet.") + return container + + for pot in history: + winner = pot.get("winner_discord_id") + amount = pot.get("winning_amount", 0) + win_type = pot.get("win_type", "DRAW") + ended = pot.get("ended_at", "?") + winner_text = f"<@{winner}>" if winner else "No winner" + container.add_text_display( + f"**{amount} STK** → {winner_text} ({win_type}) — {ended}" + ) + + return container + + +def build_winner_announcement( + winner_discord_id: str, winning_amount: int, win_type: str +) -> str: + """Build a winner announcement message for the guild channel.""" + return ( + f"🎉 **{win_type} WINNER!** 🎉\n\n" + f"<@{winner_discord_id}> has won the pot of **{winning_amount} STK**!\n" + f"Congratulations! 🎊\n\n" + f"A new pot has started — use `/enter-pot` to join!" + ) + + +def build_no_winner_announcement(pot_amount: int) -> str: + """Build a daily draw 'no winner' announcement.""" + return ( + f"🎲 Daily draw occurred, but the pot continues! No winner this time.\n" + f"Current pot: **{pot_amount} STK**\n" + f"Use `/enter-pot` to increase your chances!" + ) diff --git a/luckypot/game.py b/luckypot/game.py new file mode 100644 index 0000000..c740deb --- /dev/null +++ b/luckypot/game.py @@ -0,0 +1,425 @@ +import asyncio +import random +from functools import partial +from typing import Any, Callable, Awaitable + +from loguru import logger +from luckypot import db, stk +from stackcoin import RequestAcceptedData, RequestDeniedData + +POT_ENTRY_COST = 5 +DAILY_DRAW_CHANCE = 0.6 +RANDOM_WIN_CHANCE = 0.05 + +# Guild-bound announce functions (guild_id already applied via partial) +AnnounceFn = Callable[[str], Awaitable[Any]] | None +EditAnnounceFn = Callable[[Any, str], Awaitable[Any]] | None + +# Raw announce functions that take guild_id as first arg +RawAnnounceFn = Callable[[str, str], Awaitable[Any]] | None +RawEditAnnounceFn = Callable[[str, Any, str], Awaitable[Any]] | None + + +async def enter_pot( + discord_id: str, guild_id: str, announce_fn: AnnounceFn = None +) -> dict: + """Core pot entry logic. + + 1. Looks up the user's StackCoin account by Discord ID. + 2. Ensures an active pot exists for the guild. + 3. Prevents duplicate entries. + 4. Creates a STK *request* (bot requests payment from the user). + 5. Records a pending entry in the local DB. + 6. Rolls for an instant-win. + + Returns a result dict with at least a `status` key: + - pending - request created, waiting for user to accept + - instant_win - user got an instant win (still needs payment confirmation) + - already_entered - user is already in the pot + - error - something went wrong (see ``message`` key) + """ + + # Look up StackCoin user + stk_user = await stk.get_user_by_discord_id(discord_id) + if stk_user is None: + return { + "status": "error", + "message": "You don't have a StackCoin account. Please register first.", + } + + conn = db.get_connection() + try: + pot = db.ensure_active_pot(conn, guild_id) + pot_id = pot["pot_id"] + + if db.has_user_entered(conn, pot_id, discord_id): + return { + "status": "already_entered", + "message": "You have already entered this pot!", + } + + if db.has_pending_instant_wins(conn, guild_id): + return { + "status": "error", + "message": "An instant win is being processed. Please try again shortly.", + } + + stk_user_id = stk_user["id"] + idempotency_key = f"pot_entry:{pot_id}:{discord_id}" + req = await stk.create_request( + to_user_id=stk_user_id, + amount=POT_ENTRY_COST, + label=f"LuckyPot entry (pot #{pot_id})", + idempotency_key=idempotency_key, + ) + if req is None: + return { + "status": "error", + "message": "Failed to create StackCoin payment request.", + } + + request_id = str(req["request_id"]) + + is_instant_win = random.random() < RANDOM_WIN_CHANCE + initial_status = "pending" + + entry_id = db.add_entry( + conn, + pot_id=pot_id, + discord_id=discord_id, + amount=POT_ENTRY_COST, + stackcoin_request_id=request_id, + status=initial_status, + ) + + if is_instant_win: + db.mark_entry_instant_win(conn, entry_id) + logger.info( + f"Instant win rolled for discord_id={discord_id} entry_id={entry_id}" + ) + return { + "status": "instant_win", + "entry_id": entry_id, + "request_id": request_id, + "message": "You rolled an INSTANT WIN! Accept the payment request to claim your prize!", + } + + return { + "status": "pending", + "entry_id": entry_id, + "request_id": request_id, + "message": f"Entry submitted! Accept the {POT_ENTRY_COST} STK request to confirm your spot.", + } + finally: + conn.close() + + +def select_random_winner(participants: list[dict]) -> dict | None: + """Select a random winner from a list of participant entry dicts. + + Each participant has a `discord_id` and `amount` field. Selection is + weighted by contribution amount (though normally everyone pays the same). + """ + if not participants: + return None + + total_weight = sum(p["amount"] for p in participants) + roll = random.uniform(0, total_weight) + cumulative = 0 + for p in participants: + cumulative += p["amount"] + if roll <= cumulative: + return p + return participants[-1] + + +async def send_winnings_to_user( + winner_discord_id: str, amount: int, idempotency_key: str | None = None +) -> bool: + """Send STK winnings to the winner, checking bot balance first.""" + bot_balance = await stk.get_bot_balance() + if bot_balance is None: + logger.error("Could not check bot balance") + return False + if bot_balance < amount: + logger.error( + f"Bot balance ({bot_balance}) insufficient to pay {amount} STK to {winner_discord_id}" + ) + return False + + stk_user = await stk.get_user_by_discord_id(winner_discord_id) + if stk_user is None: + logger.error( + f"Could not find StackCoin user for discord_id={winner_discord_id}" + ) + return False + + result = await stk.send_stk( + to_user_id=stk_user["id"], + amount=amount, + label="LuckyPot winnings", + idempotency_key=idempotency_key, + ) + return result is not None + + +async def process_pot_win( + conn, + guild_id: str, + winner_id: str, + winning_amount: int, + win_type: str = "DAILY DRAW", + announce_fn: AnnounceFn = None, + edit_announce_fn: EditAnnounceFn = None, +) -> bool: + """Process a pot win: send winnings and update DB. + + For daily draws, performs a dramatic staged reveal by sending a message + and editing it through several stages with delays. + """ + pot = db.get_active_pot(conn, guild_id) + if pot is None: + logger.warning(f"No active pot found for guild {guild_id}") + return False + + idempotency_key = f"pot_win:{pot['pot_id']}:{winner_id}" + sent = await send_winnings_to_user( + winner_id, winning_amount, idempotency_key=idempotency_key + ) + if sent: + db.end_pot(conn, pot["pot_id"], winner_id, winning_amount, win_type) + logger.info( + f"Pot #{pot['pot_id']} won by {winner_id} for {winning_amount} STK ({win_type})" + ) + if announce_fn and edit_announce_fn: + await _dramatic_draw_reveal( + announce_fn, edit_announce_fn, winner_id, winning_amount, win_type + ) + elif announce_fn: + await announce_fn(f"<@{winner_id}> won {winning_amount} STK! ({win_type})") + else: + logger.error(f"Failed to send winnings to {winner_id}, pot remains active") + if announce_fn: + await announce_fn( + f"Failed to send winnings to <@{winner_id}>. The pot remains active." + ) + return sent + + +async def _dramatic_draw_reveal( + announce_fn: Callable[[str], Awaitable[Any]], + edit_announce_fn: Callable[[Any, str], Awaitable[Any]], + winner_id: str, + winning_amount: int, + win_type: str, +) -> None: + """Send a staged dramatic reveal for a pot draw.""" + label = win_type.lower() + msg = await announce_fn(f"Time for the {label}!") + if msg is None: + return + + await asyncio.sleep(3) + msg = await edit_announce_fn(msg, "The winner is...") + + await asyncio.sleep(3) + if msg: + await edit_announce_fn( + msg, + f"<@{winner_id}> has won the {label} of {winning_amount} STK!", + ) + + +async def end_pot_with_winner( + guild_id: str, + win_type: str = "DAILY DRAW", + announce_fn: AnnounceFn = None, + edit_announce_fn: EditAnnounceFn = None, +) -> bool: + """End a pot by selecting and paying a winner.""" + conn = db.get_connection() + try: + pot = db.get_active_pot(conn, guild_id) + if pot is None: + logger.info(f"No active pot for guild {guild_id}, nothing to draw") + return False + + participants = db.get_pot_participants(conn, pot["pot_id"]) + if not participants: + logger.info(f"No participants in pot #{pot['pot_id']}, skipping draw") + return False + + winner = select_random_winner(participants) + if winner is None: + return False + + total_pot = sum(p["amount"] for p in participants) + return await process_pot_win( + conn, + guild_id=guild_id, + winner_id=winner["discord_id"], + winning_amount=total_pot, + win_type=win_type, + announce_fn=announce_fn, + edit_announce_fn=edit_announce_fn, + ) + finally: + conn.close() + + +async def daily_pot_draw( + announce: RawAnnounceFn = None, + edit_announce: RawEditAnnounceFn = None, +): + """Daily pot draw + + For each guild with an active pot, rolls DAILY_DRAW_CHANCE to decide + whether to draw a winner. If no draw, the pot carries over. + + ``announce`` and ``edit_announce`` are the raw bot functions that take + ``guild_id`` as their first argument. Per-guild partials are created + internally for each guild being drawn. + """ + conn = db.get_connection() + try: + guilds = db.get_all_active_guilds(conn) + finally: + conn.close() + + for guild_id in guilds: + check_conn = db.get_connection() + try: + has_pending = db.has_pending_instant_wins(check_conn, guild_id) + finally: + check_conn.close() + + if has_pending: + logger.info( + f"Skipping daily draw for guild {guild_id}: pending instant wins" + ) + continue + + roll = random.random() + if roll < DAILY_DRAW_CHANCE: + logger.info(f"Daily draw triggered for guild {guild_id} (roll={roll:.3f})") + guild_announce = partial(announce, guild_id) if announce else None + guild_edit = partial(edit_announce, guild_id) if edit_announce else None + await end_pot_with_winner( + guild_id, + win_type="DAILY DRAW", + announce_fn=guild_announce, + edit_announce_fn=guild_edit, + ) + else: + logger.info( + f"Daily draw skipped for guild {guild_id} (roll={roll:.3f}, needed < {DAILY_DRAW_CHANCE})" + ) + + +async def on_request_accepted(event_data: RequestAcceptedData, announce: RawAnnounceFn = None): + """Handle a payment request being accepted. + + When a user accepts the pot entry payment, we confirm their entry. + If it was an instant win, we immediately process the win. + + ``announce`` is the raw announce function that takes ``(guild_id, message)`` + — the guild is looked up from the DB entry, not from the event payload. + """ + request_id = str(event_data.request_id) + if not request_id: + logger.warning("on_request_accepted called without request_id") + return + + conn = db.get_connection() + try: + entry = db.get_entry_by_request_id(conn, request_id) + if entry is None: + logger.debug( + f"Request {request_id} not associated with any pot entry (ignoring)" + ) + return + + entry_id = entry["entry_id"] + guild_id = entry["pot_guild_id"] + discord_id = entry["discord_id"] + announce_fn = partial(announce, guild_id) if announce else None + + if entry["status"] == "instant_win": + db.confirm_entry(conn, entry_id) + logger.info( + f"Instant win confirmed for entry {entry_id}, discord_id={discord_id}" + ) + + pot = db.get_active_pot(conn, guild_id) + if pot is None: + logger.error(f"No active pot for guild {guild_id} during instant win") + return + + participants = db.get_pot_participants(conn, pot["pot_id"]) + total_pot = sum(p["amount"] for p in participants) + + if announce_fn: + await announce_fn( + f"<@{discord_id}> entered the pot and rolled an INSTANT WIN! The pot was at {total_pot} STK!" + ) + + await process_pot_win( + conn, + guild_id=guild_id, + winner_id=discord_id, + winning_amount=total_pot, + win_type="INSTANT WIN", + announce_fn=announce_fn, + ) + elif entry["status"] == "pending": + db.confirm_entry(conn, entry_id) + logger.info(f"Entry {entry_id} confirmed for discord_id={discord_id}") + if announce_fn: + pot = db.get_active_pot(conn, guild_id) + total_pot = 0 + if pot: + participants = db.get_pot_participants(conn, pot["pot_id"]) + total_pot = sum(p["amount"] for p in participants) + await announce_fn( + f"<@{discord_id}> entered the pot! The pot is now at {total_pot} STK. Use `/enter-pot` to enter!" + ) + else: + logger.warning( + f"Request accepted for entry {entry_id} in unexpected status: {entry['status']}" + ) + finally: + conn.close() + + +async def on_request_denied(event_data: RequestDeniedData, announce: RawAnnounceFn = None): + """Handle a payment request being denied. + + ``announce`` is the raw announce function that takes ``(guild_id, message)``. + """ + request_id = str(event_data.request_id) + if not request_id: + logger.warning("on_request_denied called without request_id") + return + + conn = db.get_connection() + try: + entry = db.get_entry_by_request_id(conn, request_id) + if entry is None: + logger.debug( + f"Request {request_id} not associated with any pot entry (ignoring)" + ) + return + + entry_id = entry["entry_id"] + guild_id = entry["pot_guild_id"] + discord_id = entry["discord_id"] + announce_fn = partial(announce, guild_id) if announce else None + + db.deny_entry(conn, entry_id) + logger.info(f"Entry {entry_id} denied for discord_id={discord_id}") + if announce_fn: + await announce_fn( + f"<@{discord_id}>'s pot entry was cancelled (payment denied)." + ) + finally: + conn.close() diff --git a/luckypot/stk.py b/luckypot/stk.py new file mode 100644 index 0000000..b20e8bd --- /dev/null +++ b/luckypot/stk.py @@ -0,0 +1,124 @@ +"""StackCoin API access for LuckyPot. + +Thin wrapper around the stackcoin SDK that manages a shared Client instance +configured from LuckyPot's settings. +""" + +import stackcoin +from loguru import logger + +from luckypot.config import settings + +_client: stackcoin.Client | None = None + + +def get_client() -> stackcoin.Client: + """Get or create the shared StackCoin client.""" + global _client + if _client is None: + _client = stackcoin.Client( + base_url=settings.stackcoin_api_url, + token=settings.stackcoin_api_token, + ) + return _client + + +def reset_client() -> None: + """Reset the client (for testing or config changes).""" + global _client + _client = None + + +async def get_user_by_discord_id(discord_id: str) -> dict | None: + """Look up a StackCoin user by their Discord ID.""" + try: + users = await get_client().get_users(discord_id=discord_id) + if not users: + return None + user = users[0] + return {"id": user.id, "username": user.username, "balance": user.balance} + except stackcoin.StackCoinError as e: + logger.error(f"Failed to look up user by discord_id={discord_id}: {e}") + return None + + +async def get_bot_user() -> dict | None: + """Get the bot's own StackCoin user profile.""" + try: + user = await get_client().get_me() + return {"id": user.id, "username": user.username, "balance": user.balance} + except stackcoin.StackCoinError as e: + logger.error(f"Failed to get bot user: {e}") + return None + + +async def get_bot_balance() -> int | None: + """Get the bot's current STK balance.""" + try: + user = await get_client().get_me() + return user.balance + except stackcoin.StackCoinError as e: + logger.error(f"Failed to get bot balance: {e}") + return None + + +async def send_stk( + to_user_id: int, amount: int, label: str | None = None, idempotency_key: str | None = None +) -> dict | None: + """Send STK to a user. Returns response dict or None on failure.""" + try: + result = await get_client().send( + to_user_id=to_user_id, amount=amount, label=label, idempotency_key=idempotency_key + ) + return { + "success": result.success, + "transaction_id": result.transaction_id, + "amount": result.amount, + "from_new_balance": result.from_new_balance, + "to_new_balance": result.to_new_balance, + } + except stackcoin.StackCoinError as e: + logger.error(f"Failed to send {amount} STK to user {to_user_id}: {e}") + return None + + +async def create_request( + to_user_id: int, amount: int, label: str | None = None, idempotency_key: str | None = None +) -> dict | None: + """Create a payment request. Returns response dict or None on failure.""" + try: + result = await get_client().create_request( + to_user_id=to_user_id, amount=amount, label=label, idempotency_key=idempotency_key + ) + return { + "success": result.success, + "request_id": result.request_id, + "amount": result.amount, + "status": result.status, + } + except stackcoin.StackCoinError as e: + logger.error(f"Failed to create request for {amount} STK from user {to_user_id}: {e}") + return None + + +async def get_request(request_id: int) -> dict | None: + """Get a request by ID.""" + try: + req = await get_client().get_request(request_id=request_id) + return { + "id": req.id, + "amount": req.amount, + "status": req.status, + } + except stackcoin.StackCoinError as e: + logger.error(f"Failed to get request {request_id}: {e}") + return None + + +async def get_guild_channel(guild_id: str) -> str | None: + """Get the designated channel for a Discord guild.""" + try: + guild = await get_client().get_discord_guild(snowflake=guild_id) + return guild.designated_channel_snowflake + except stackcoin.StackCoinError: + return None diff --git a/pyproject.toml b/pyproject.toml index baa29ea..0b01e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,20 @@ [project] name = "luckypot" version = "0.1.0" -description = "LuckyPot is a bot that implments a lottery system ontop of StackCoin" +description = "LuckyPot is a bot that implements a lottery system on top of StackCoin" readme = "README.md" requires-python = ">=3.13" dependencies = [ "hikari>=2.3.5", "hikari-lightbulb>=3.1.1", "loguru>=0.7.3", - "python-dotenv>=1.1.1", - "schedule>=1.2.0", + "pydantic-settings>=2.0", + "stackcoin>=0.1.0", ] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["luckypot"] diff --git a/stk.py b/stk.py deleted file mode 100644 index 78d5009..0000000 --- a/stk.py +++ /dev/null @@ -1,187 +0,0 @@ -from loguru import logger -from stackcoin_python import AuthenticatedClient -from stackcoin_python.types import Unset -from stackcoin_python.models import ( - UsersResponse, - CreateRequestParams, - CreateRequestResponse, - RequestsResponse, - SendStkParams, - SendStkResponse, - DiscordGuildResponse, - User, - Request, -) -from stackcoin_python.api.default import ( - stackcoin_users, - stackcoin_create_request, - stackcoin_requests, - stackcoin_deny_request, - stackcoin_discord_guild, - stackcoin_send_stk, -) -import config - - -def get_client(): - """Get authenticated StackCoin client""" - if not config.STACKCOIN_BASE_URL or not config.STACKCOIN_BOT_TOKEN: - raise ValueError("StackCoin credentials not configured") - return AuthenticatedClient( - base_url=config.STACKCOIN_BASE_URL, token=config.STACKCOIN_BOT_TOKEN - ) - - -async def get_user_by_discord_id(discord_id: str) -> User | None: - """Get StackCoin user by Discord ID""" - try: - async with get_client() as client: - response = await stackcoin_users.asyncio( - client=client, discord_id=discord_id - ) - - if not isinstance(response, UsersResponse) or isinstance( - response.users, Unset - ): - logger.error(f"Failed to get user info for Discord ID {discord_id}") - return None - - if len(response.users) == 0: - logger.warning(f"No StackCoin user found for Discord ID {discord_id}") - return None - - return response.users[0] - except Exception as e: - logger.error(f"Error getting user by Discord ID {discord_id}: {e}") - return None - - -async def create_payment_request( - user_id: int, amount: int, label: str = "Lucky Pot Entry" -) -> str | None: - """Create a payment request and return request ID""" - try: - async with get_client() as client: - response = await stackcoin_create_request.asyncio( - client=client, - user_id=user_id, - body=CreateRequestParams(amount=amount, label=label), - ) - - if isinstance(response, CreateRequestResponse): - return str(response.request_id) - else: - logger.error(f"Failed to create payment request for user {user_id}") - return None - except Exception as e: - logger.error(f"Error creating payment request for user {user_id}: {e}") - return None - - -async def send_stk(discord_id: str, amount: int, label: str) -> bool: - """Send STK to a user by Discord ID""" - try: - user = await get_user_by_discord_id(discord_id) - if not user: - return False - - async with get_client() as client: - if not isinstance(user.id, int): - raise Exception("User ID is not an integer") - - response = await stackcoin_send_stk.asyncio( - client=client, - user_id=user.id, - body=SendStkParams(amount=amount, label=label), - ) - - if isinstance(response, SendStkResponse) and response.success: - logger.info(f"Successfully sent {amount} STK to {user.username}") - return True - else: - logger.error( - f"Failed to send {amount} STK to {user.username}, response: {response}" - ) - return False - except Exception as e: - logger.error(f"Error sending {amount} STK to Discord ID {discord_id}: {e}") - return False - - -async def get_accepted_requests() -> list[Request]: - """Get all accepted payment requests from the last 2 hours with pagination""" - try: - all_requests = [] - page = 1 - - async with get_client() as client: - while True: - response = await stackcoin_requests.asyncio( - client=client, - role="responder", - status="accepted", - since="2h", - page=page, - ) - - if not isinstance(response, RequestsResponse) or isinstance( - response.requests, Unset - ): - break - - if not response.requests: - break - - all_requests.extend(response.requests) - - total_pages = ( - response.pagination.total_pages - if not isinstance(response.pagination, Unset) - else None - ) - - if ( - isinstance(response.pagination, Unset) - or total_pages is None - or isinstance(total_pages, Unset) - or page >= total_pages - ): - break - - page += 1 - - return all_requests - except Exception as e: - logger.error(f"Error getting accepted requests: {e}") - return [] - - -async def deny_request(request_id: str) -> bool: - """Deny a payment request""" - try: - async with get_client() as client: - await stackcoin_deny_request.asyncio( - client=client, request_id=int(request_id) - ) - return True - except Exception as e: - logger.error(f"Error denying request {request_id}: {e}") - return False - - -async def get_guild_channel(guild_id: str) -> str | None: - """Get designated channel for a guild""" - try: - async with get_client() as client: - response = await stackcoin_discord_guild.asyncio( - client=client, snowflake=guild_id - ) - - if isinstance(response, DiscordGuildResponse) and not isinstance( - response.designated_channel_snowflake, Unset - ): - return response.designated_channel_snowflake - return None - except Exception as e: - logger.error(f"Error getting guild channel for {guild_id}: {e}") - return None diff --git a/uv.lock b/uv.lock index a42907f..611a468 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,14 @@ version = 1 +revision = 3 requires-python = ">=3.13" [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] @@ -23,25 +24,25 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741 }, - { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407 }, - { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703 }, - { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532 }, - { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794 }, - { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865 }, - { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238 }, - { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566 }, - { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270 }, - { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294 }, - { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958 }, - { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553 }, - { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688 }, - { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157 }, - { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050 }, - { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647 }, - { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067 }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, ] [[package]] @@ -51,36 +52,66 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -90,61 +121,70 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, ] [[package]] name = "confspec" version = "0.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/59/d236c71e2c4f9c43735f973638c43936658d9139115896316f6b6d300fbc/confspec-0.0.3.tar.gz", hash = "sha256:1d421b4e3342df6ea094119728ad03f89aff06b75b813db37a6726ca63e702fc", size = 59885 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/59/d236c71e2c4f9c43735f973638c43936658d9139115896316f6b6d300fbc/confspec-0.0.3.tar.gz", hash = "sha256:1d421b4e3342df6ea094119728ad03f89aff06b75b813db37a6726ca63e702fc", size = 59885, upload-time = "2025-05-24T16:10:26.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/eb/2a56b42535249452642199adb4c683c3332658762a15dd2e54772f63dfcb/confspec-0.0.3-py3-none-any.whl", hash = "sha256:0deaf3cccefbf55a4a2184bdcf5806fedb8b9343ba19641393f31dfd6fe2e78e", size = 15918 }, + { url = "https://files.pythonhosted.org/packages/19/eb/2a56b42535249452642199adb4c683c3332658762a15dd2e54772f63dfcb/confspec-0.0.3-py3-none-any.whl", hash = "sha256:0deaf3cccefbf55a4a2184bdcf5806fedb8b9343ba19641393f31dfd6fe2e78e", size = 15918, upload-time = "2025-05-24T16:10:24.961Z" }, ] [[package]] name = "frozenlist" version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078 } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { 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/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791 }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165 }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881 }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409 }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132 }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638 }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539 }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646 }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233 }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996 }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280 }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717 }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644 }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879 }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502 }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169 }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219 }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345 }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880 }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498 }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296 }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103 }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869 }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467 }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028 }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294 }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898 }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465 }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385 }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771 }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206 }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620 }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059 }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516 }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106 }, + { 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]] @@ -157,9 +197,9 @@ dependencies = [ { name = "colorlog" }, { name = "multidict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/f9/0d0e2767af45b8b8ee378588f22e4e331f6c3823ac08a396319a65805d3a/hikari-2.3.5.tar.gz", hash = "sha256:33ab457680bb36edea93730952d7bda6174c8c26e5dcfdb9a45b91f0548fb089", size = 1892942 } +sdist = { url = "https://files.pythonhosted.org/packages/85/f9/0d0e2767af45b8b8ee378588f22e4e331f6c3823ac08a396319a65805d3a/hikari-2.3.5.tar.gz", hash = "sha256:33ab457680bb36edea93730952d7bda6174c8c26e5dcfdb9a45b91f0548fb089", size = 1892942, upload-time = "2025-06-25T17:37:18.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/81/9c4bcb9502d6cb7a8aab04b89006bc9d6e2a274f0bf0e17eeb156aa7d632/hikari-2.3.5-py3-none-any.whl", hash = "sha256:8ee60cbecaef34f77977d8bc7f707bf034c2b04f2c480e081d66e1943a6cdab6", size = 578267 }, + { url = "https://files.pythonhosted.org/packages/f9/81/9c4bcb9502d6cb7a8aab04b89006bc9d6e2a274f0bf0e17eeb156aa7d632/hikari-2.3.5-py3-none-any.whl", hash = "sha256:8ee60cbecaef34f77977d8bc7f707bf034c2b04f2c480e081d66e1943a6cdab6", size = 578267, upload-time = "2025-06-25T17:37:16.658Z" }, ] [[package]] @@ -173,27 +213,55 @@ dependencies = [ { name = "linkd" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/2c/d807bfa369b26c6223066b7ffa7f1d4c4b5980386f833999a1f3a0d37bd6/hikari_lightbulb-3.1.1.tar.gz", hash = "sha256:0121045a806b8b6d5574dc45fe2fe544a26c4666d3a0ed5fc05e3a72a7f27985", size = 133719 } +sdist = { url = "https://files.pythonhosted.org/packages/02/2c/d807bfa369b26c6223066b7ffa7f1d4c4b5980386f833999a1f3a0d37bd6/hikari_lightbulb-3.1.1.tar.gz", hash = "sha256:0121045a806b8b6d5574dc45fe2fe544a26c4666d3a0ed5fc05e3a72a7f27985", size = 133719, upload-time = "2025-07-30T17:28:14.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/29/d4b7ea545e1ad8b7e55cf83c4dd7c0f33f8fdc64a8d698a8ee9237a420e0/hikari_lightbulb-3.1.1-py3-none-any.whl", hash = "sha256:c6548f5b897e28b0185f3539209af75d3e36c478916702d3b148f9ee1ece670c", size = 101734, upload-time = "2025-07-30T17:28:13.176Z" }, +] + +[[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/b2/29/d4b7ea545e1ad8b7e55cf83c4dd7c0f33f8fdc64a8d698a8ee9237a420e0/hikari_lightbulb-3.1.1-py3-none-any.whl", hash = "sha256:c6548f5b897e28b0185f3539209af75d3e36c478916702d3b148f9ee1ece670c", size = 101734 }, + { 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" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "linkd" version = "0.0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/de/4aa22044fab18f681a9f5d63e8e18ec41683f6b478da464f6d413eebc906/linkd-0.0.10.tar.gz", hash = "sha256:40b7ba0e01373bcb9baa6118d8ae439f2d40a0a9dda7e436fde3965b970c5cb0", size = 43224 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/de/4aa22044fab18f681a9f5d63e8e18ec41683f6b478da464f6d413eebc906/linkd-0.0.10.tar.gz", hash = "sha256:40b7ba0e01373bcb9baa6118d8ae439f2d40a0a9dda7e436fde3965b970c5cb0", size = 43224, upload-time = "2025-07-25T15:29:56.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/48/8a5992965a029691237ddb5c9306a12e7ff8296e52bf8f47230abd535b2b/linkd-0.0.10-py3-none-any.whl", hash = "sha256:d52a19109c3b9be699f096b29a0d2554f51417e6b30b85bf32e5f7d41c72e567", size = 38857 }, + { url = "https://files.pythonhosted.org/packages/43/48/8a5992965a029691237ddb5c9306a12e7ff8296e52bf8f47230abd535b2b/linkd-0.0.10-py3-none-any.whl", hash = "sha256:d52a19109c3b9be699f096b29a0d2554f51417e6b30b85bf32e5f7d41c72e567", size = 38857, upload-time = "2025-07-25T15:29:55.571Z" }, ] [[package]] @@ -204,152 +272,275 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "win32-setctime", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] [[package]] name = "luckypot" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "hikari" }, { name = "hikari-lightbulb" }, + { name = "httpx" }, { name = "loguru" }, - { name = "python-dotenv" }, - { name = "schedule" }, + { name = "pydantic-settings" }, + { name = "websockets" }, ] [package.metadata] requires-dist = [ { name = "hikari", specifier = ">=2.3.5" }, { name = "hikari-lightbulb", specifier = ">=3.1.1" }, + { name = "httpx", specifier = ">=0.27.0" }, { name = "loguru", specifier = ">=0.7.3" }, - { name = "python-dotenv", specifier = ">=1.1.1" }, - { name = "schedule", specifier = ">=1.2.0" }, + { name = "pydantic-settings", specifier = ">=2.0" }, + { name = "websockets", specifier = ">=13.0" }, ] [[package]] name = "multidict" version = "6.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843 }, - { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053 }, - { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273 }, - { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124 }, - { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892 }, - { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547 }, - { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223 }, - { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262 }, - { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345 }, - { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248 }, - { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115 }, - { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649 }, - { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203 }, - { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051 }, - { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601 }, - { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683 }, - { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811 }, - { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056 }, - { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811 }, - { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304 }, - { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775 }, - { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773 }, - { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083 }, - { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980 }, - { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776 }, - { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882 }, - { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816 }, - { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341 }, - { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854 }, - { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432 }, - { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731 }, - { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086 }, - { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338 }, - { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812 }, - { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011 }, - { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254 }, - { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313 }, + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, + { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, + { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, + { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, + { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] name = "propcache" version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[package]] -name = "python-dotenv" -version = "1.1.1" +name = "pydantic" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] -name = "schedule" -version = "1.2.2" +name = "pydantic-core" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/91/b525790063015759f34447d4cf9d2ccb52cdee0f1dd6ff8764e863bcb74c/schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7", size = 26452 } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "win32-setctime" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] [[package]] @@ -361,41 +552,41 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 } +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ]