diff --git a/.gitignore b/.gitignore index a863bc76..e20118e6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,10 @@ .secrets .idea .vscode -README.md -src/europython_discord/config.local.toml __pycache__ .DS_Store registered_log.txt -schedule.json +schedule_cache.json pretix_cache.json *.egg-info/ +livestreams.toml diff --git a/Dockerfile b/Dockerfile index 1cc49ea6..b502f673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,9 @@ RUN groupadd --gid 1000 bot && \ USER bot WORKDIR /home/bot -ENV PATH="/home/bot/.local/bin:$PATH" - COPY --chown=bot:bot pyproject.toml uv.lock ./ COPY --chown=bot:bot src ./src RUN uv sync -ENTRYPOINT ["uv", "run", "run-bot"] +ENTRYPOINT ["uv", "run", "run-bot", "--config-file", "prod-config.toml"] diff --git a/README.md b/README.md index a98ed24f..77c60666 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,95 @@ -# Europython Discord Bot +# Europython Discord -An easy to deploy conference bot that manages roles for attendees via registration, notifies about upcoming sessions. -Exposes Discord server statistics to organizers. -We hosted the bot on Hetzner. And deployed with a single click Action from GitHub 😎. +A suite of tools for managing the EuroPython Conference Discord server: -![registration_view.png](./img/registration_view.png) +* [src/europython_discord](./src/europython_discord): Discord bot +* [scripts/configure-guild.py](./scripts/configure-guild.py): Configure channels and roles of a Discord server +* [scripts/export-members.py](./scripts/export-members.py): Export a list of all server members and their roles -## Overview +The scripts work standalone and only require an Auth token. Please find more documentation in the respective files. -The `main` method in `src/europython_discord/bot.py` is the entry point for the bot. -I't a good starting point to start browsing the codebase. -It requires a `.secrets` file in the root of the repository with `DISCORD_BOT_TOKEN` and `PRETIX_TOKEN` environment variables. +The bot has the following extensions ("Cogs"): -### Registration +* Ping: To check if the bot is running, write `$ping` in any channel. The bot will respond with `Pong!`. +* Guild Statistics: As an organizer, write `$participants` in an organizer-only channel. The bot will respond with a list of roles, and the number of members per role. +* Registration: On startup, the bot posts a registration form. New users must register using their Pretix ticket data. On success, the bot assigns the appropriate roles. +* Programme Notifications: Before each session, the bot posts a session summary and updates the livestream URLs. -At EuroPython, we use [pretix](https://pretix.eu/about/en/) as our ticketing system. +## Screenshots +### Registration Channel: +![Registration Channel](./img/registration-channel.png) -The bot utilizes the Pretix API to fetch ticket information and creates an in-memory key-value store to retrieve the ticket type for a given Discord user. The mapping between ticket types and Discord roles is defined in a JSON file, such as ticket_to_roles_prod.json, and is used by the bot to assign roles to users. +### Registration Form: +![Registration Form](./img/registration-form.png) -There are safeguard methods in place to prevent users from registering multiple times and to make a direct Pretix API call in case the user information is not available in the in-memory store. +### Programme Notification: +![Programme Notification](./img/programme-notification.png) +## Configuration -### Program notifications +All configuration is server-agnostic. You can set up your own Discord server and use the included configuration. -Is a service to push the programme notification to Discord. Pretalx API is used to fetch the programme information, and `config.toml` holds information about livestream URLs. +Arguments and environment variables: -### Organizers extension +* Argument `--config-file`: Path to .toml configuration file +* Environment variable `DISCORD_BOT_TOKEN`: Discord bot auth token (with Admin and `GUILD_MEMBERS` privileges) +* Environment variable `PRETIX_TOKEN`: Pretix access token (preferably read-only) -A set of commands that are available only for organizers that are allowing to get statistics about the Discord server. +Included example configuration files: + +* [`prod-config.toml`](./prod-config.toml) or [`test-config.toml`](./test-config.toml): Prod/Test configuration +* [`test-livestreams.toml`](./test-livestreams.toml): Test livestream URL configuration + +Used cache and log files (will be created if necessary): + +* `pretix_cache.json`: Local cache of Pretix ticket data +* `registered_log.txt`: Log of registered users +* `schedule_cache.json`: Local cache of [programapi](https://github.com/europython/programapi) schedule ## Setup ### Quickstart using `pip` This project uses [uv](https://github.com/astral-sh/uv) for managing dependencies. -If you just want to try the bot and skip all the development setup, +If you just want to try the bot and skip the development setup, you can use `pip` instead of `uv` (requires Python >= 3.11): ```shell # create and activate virtual environment (optional, but recommended) python -m venv .venv -. .venv/bin/activate # Windows: '.venv/Scripts/activate' +. .venv/bin/activate # Windows: .venv/Scripts/activate -# install this package +# install this package (use '-e' for 'editable mode' if you plan to modify the code) pip install . -# run the bot -run-bot +# set environment variables +export DISCORD_BOT_TOKEN=... # Windows: $env:DISCORD_BOT_TOKEN = '...' +export PRETIX_TOKEN=... # Windows: $env:PRETIX_TOKEN = '...' + +# run the bot with a given config file +run-bot --config your-config-file.toml ``` ### Development setup using `uv` -Install `uv` as documented [here](https://docs.astral.sh/uv/getting-started/installation/), then -create/update virtual environment with all dependencies according to [`uv.lock`](./uv.lock) -with `uv sync --dev`. +Install `uv` as documented [here](https://docs.astral.sh/uv/getting-started/installation/), then run `uv sync --dev` to create/update a +virtual environment with all dependencies according to [`uv.lock`](./uv.lock). If required, `uv` will download the required Python version, as specified in [`.python-version`](./.python-version). -### Using `uv` +To run the bot, use the following: + +```shell +# set environment variables +export DISCORD_BOT_TOKEN=... # Windows: $env:DISCORD_BOT_TOKEN = '...' +export PRETIX_TOKEN=... # Windows: $env:PRETIX_TOKEN = '...' + +# run the bot with a given config file +uv run run-bot --config your-config-file.toml +``` + +#### Useful `uv` commands -This is a summary of useful `uv` commands. Please refer to the [uv documentation](https://docs.astral.sh/uv) or `uv help` for details. ```shell @@ -70,19 +100,22 @@ uv sync --dev # include dev dependencies # activate uv-generated venv . .venv/bin/activate # Windows: '.venv/Scripts/activate' +# execute command inside uv-generated venv +uv run [command] + # reset all packages to versions pinned in uv.lock uv sync uv sync --dev # include dev dependencies # add package -uv add package -uv add --dev package # install as dev dependency +uv add [package] +uv add --dev [package] # install as dev dependency # upgrade packages uv lock --upgrade # remove package -uv remove package +uv remove [package] ``` ### Development tools @@ -93,26 +126,14 @@ uv remove package * Check code style: `uv run --dev ruff check .` * Run tests: `uv run --dev pytest .` -### Configuration - -Create `config.local.toml` file in the `src/europython_discord` directory, it would be used instead of `config.toml` if exists. - -Add `.secrets` file to the root of the repository with the following content: - -```shell -DISCORD_BOT_TOKEN= -PRETIX_TOKEN= -```` - -After you have added the `.secrets` file, you can run the bot with the following command: +### Deployment -```shell -run-bot -``` +The bot is deployed on a VPS using a GitHub Action. +It uses Ansible to configure the VPS, and Docker Compose to run the bot. -or with docker: +Related files: -```shell -docker build --tag discord_bot . -docker run --interactive --tty --env DISCORD_BOT_TOKEN=$DISCORD_BOT_TOKEN --env PRETIX_TOKEN=$PRETIX_TOKEN discord_bot -``` +* [.github/workflows/deploy.yml](./.github/workflows/deploy.yml): The GitHub Action +* [ansible/deploy-playbook.yml](./ansible/deploy-playbook.yml): The Ansible Playbook +* [Dockerfile](./Dockerfile): The Docker container recipe +* [compose.yaml](./compose.yaml): The Docker Compose recipe diff --git a/ansible/deploy-playbook.yml b/ansible/deploy-playbook.yml index 44deda1e..cb115642 100644 --- a/ansible/deploy-playbook.yml +++ b/ansible/deploy-playbook.yml @@ -5,7 +5,6 @@ repository_url: https://github.com/EuroPython/discord.git tasks: - - name: Enable persistent logging for journald ini_file: path: /etc/systemd/journald.conf @@ -15,7 +14,6 @@ no_extra_spaces: true backup: true - - name: reload systemd-journald systemd: name: systemd-journald @@ -94,9 +92,9 @@ owner: bot group: bot - - name: Create schedule.json in bot's home directory + - name: Create schedule_cache.json in bot's home directory file: - path: /home/bot/schedule.json + path: /home/bot/schedule_cache.json state: touch owner: bot group: bot diff --git a/compose.yaml b/compose.yaml index c9eb836f..6d1b4489 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,15 +1,18 @@ services: EuroPythonBot: image: europythonbot - build: . + build: + context: . + env_file: + - /root/.secrets volumes: - type: bind - source: /etc/EuroPython/discord/.secrets - target: /home/bot/.secrets + source: prod-config.toml + target: /home/bot/prod-config.toml read_only: true - type: bind - source: /etc/EuroPython/livestreams/livestreams.toml + source: /root/livestreams.toml target: /home/bot/livestreams.toml read_only: true @@ -19,8 +22,8 @@ services: read_only: false - type: bind - source: /home/bot/schedule.json - target: /home/bot/schedule.json + source: /home/bot/schedule_cache.json + target: /home/bot/schedule_cache.json read_only: false - type: bind diff --git a/img/programme-notification.png b/img/programme-notification.png new file mode 100644 index 00000000..35b71421 Binary files /dev/null and b/img/programme-notification.png differ diff --git a/img/registration-channel.png b/img/registration-channel.png new file mode 100644 index 00000000..b63d9d87 Binary files /dev/null and b/img/registration-channel.png differ diff --git a/img/registration-form.png b/img/registration-form.png new file mode 100644 index 00000000..2736c368 Binary files /dev/null and b/img/registration-form.png differ diff --git a/img/registration_view.png b/img/registration_view.png deleted file mode 100644 index 25402788..00000000 Binary files a/img/registration_view.png and /dev/null differ diff --git a/prod-config.toml b/prod-config.toml new file mode 100644 index 00000000..a89dfdd9 --- /dev/null +++ b/prod-config.toml @@ -0,0 +1,57 @@ +log_level = "INFO" + +[registration] +registration_form_channel_name = "registration-form" +registration_help_channel_name = "registration-help" +registration_log_channel_name = "registration-log" + +pretix_base_url = "https://pretix.eu/api/v1/organizers/europython/events/ep2025" + +registered_cache_file = "registered_log.txt" +pretix_cache_file = "pretix_cache.json" + +[registration.item_to_roles] +# onsite participants +"Business" = ["Participants", "Onsite Participants"] +"Personal" = ["Participants", "Onsite Participants"] +"Education" = ["Participants", "Onsite Participants"] +"Community Contributors" = ["Participants", "Onsite Participants"] +"Grant ticket" = ["PARTICIPANTS", "Onsite Participants"] +# remote participants +"Remote Participation Ticket" = ["Participants", "Remote Participants"] +"Remote Grant ticket" = ["Participants", "Remote Participants"] +"Remote Community Organiser" = ["Participants", "Remote Participants"] +# sponsors +"Sponsor Conference Pass" = ["Participants", "Onsite Participants", "Sponsors"] +# speakers +"Presenter" = ["Participants", "Onsite Participants", "Speakers"] + +[registration.variation_to_roles] +"Volunteer" = ["Volunteers"] + +[program_notifications] +# UTC offset in hours (e.g. 2 for CEST) +timezone_offset = 2 +api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json" +schedule_cache_file = "schedule_cache.json" +livestream_url_file = "livestreams.toml" +main_notification_channel_name = "programme-notifications" + +# optional simulated start time for testing program notifications +# simulated_start_time = "2024-07-10T07:30:00+02:00" + +# optional fast mode for faster testing of program notifications +# will only take effect if simulated_start_time is set +# fast_mode = true + +[program_notifications.rooms_to_channel_names] +"Forum Hall" = "forum-hall" +"South Hall 2A" = "south-hall-2a" +"South Hall 2B" = "south-hall-2b" +"North Hall" = "north-hall" +"Terrace 2A" = "terrace-2a" +"Terrace 2B" = "terrace-2b" +"Exhibit Hall" = "exhibit-hall" + +[guild_statistics] +required_role = "Organizers" diff --git a/pyproject.toml b/pyproject.toml index 3d6a843e..5ab1c993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,17 +14,12 @@ dependencies = [ "discord-py>=2.3.1", "aiofiles>=24.1.0", "aiohttp>=3.11.16", - "arrow>=1.3.0", - "certifi>=2024.7.4", - "python-dotenv>=1.0.1", - "yarl>=1.19.0", "pydantic>=2.8.2", "unidecode>=1.3.8", ] [dependency-groups] dev = [ - "ansible>=10.2.0", "pytest>=8.3.5", "pytest-aiohttp>=1.1.0", "pytest-asyncio>=0.26.0", diff --git a/src/europython_discord/bot.py b/src/europython_discord/bot.py index 0c81f97d..73f43354 100644 --- a/src/europython_discord/bot.py +++ b/src/europython_discord/bot.py @@ -1,90 +1,72 @@ +from __future__ import annotations + +import argparse import asyncio import logging import os import sys +import tomllib from pathlib import Path +from typing import Literal import discord from discord.ext import commands -from dotenv import load_dotenv +from pydantic import BaseModel -from europython_discord import configuration -from europython_discord.cogs.ping import Ping +from europython_discord.cogs.guild_statistics import GuildStatisticsCog, GuildStatisticsConfig +from europython_discord.cogs.ping import PingCog from europython_discord.program_notifications.cog import ProgramNotificationsCog +from europython_discord.program_notifications.config import ProgramNotificationsConfig from europython_discord.registration.cog import RegistrationCog +from europython_discord.registration.config import RegistrationConfig -load_dotenv(Path(__file__).resolve().parent.parent.parent / ".secrets") -DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") - -_logger = logging.getLogger("bot") - - -class Bot(commands.Bot): - def __init__(self) -> None: - intents = _get_intents() - super().__init__(command_prefix=commands.when_mentioned_or("$"), intents=intents) - self.guild = None - self.channels = {} - - async def on_ready(self) -> None: - _logger.info("Logged in as user %r (ID=%r)", self.user.name, self.user.id) - - async def load_extension(self, name: str, *, package: str | None = None) -> None: - """Load the extension by name. +_logger = logging.getLogger(__name__) - :param name: The name of the extension to load - :param package: An optional package name for relative imports - """ - try: - await super().load_extension(name, package=package) - except commands.ExtensionError: - _logger.exception("Failed to load extension %r (package=%r):", name, package) - else: - _logger.info("Successfully loaded extension %r (package=%r)", name, package) +class Config(BaseModel): + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + registration: RegistrationConfig + program_notifications: ProgramNotificationsConfig + guild_statistics: GuildStatisticsConfig -def _setup_logging() -> None: - """Set up a basic logging configuration.""" - config = configuration.Config() - # Create a stream handler that logs to stdout (12-factor app) - stream_handler = logging.StreamHandler(stream=sys.stdout) - stream_handler.setLevel(config.LOG_LEVEL) - formatter = logging.Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s") - stream_handler.setFormatter(formatter) - - # Configure the root logger with the stream handler and log level - root_logger = logging.getLogger() - root_logger.addHandler(stream_handler) - root_logger.setLevel(config.LOG_LEVEL) - - -def _get_intents() -> discord.Intents: - """Get the desired intents for the bot.""" +async def run_bot(config: Config, auth_token: str) -> None: intents = discord.Intents.all() intents.presences = False intents.dm_typing = False intents.dm_reactions = False intents.invites = False intents.integrations = False - return intents + async with commands.Bot(intents=intents, command_prefix="$") as bot: + await bot.add_cog(PingCog(bot)) + await bot.add_cog(RegistrationCog(bot, config.registration)) + await bot.add_cog(ProgramNotificationsCog(bot, config.program_notifications)) + await bot.add_cog(GuildStatisticsCog(bot, config.guild_statistics)) -async def run_bot(bot: Bot) -> None: - _setup_logging() - async with bot: - await bot.add_cog(Ping(bot)) - await bot.add_cog(RegistrationCog(bot)) - await bot.add_cog(ProgramNotificationsCog(bot)) - await bot.load_extension("europython_discord.extensions.organisers") - await bot.start(DISCORD_BOT_TOKEN) + await bot.start(auth_token) def main() -> None: - bot = Bot() + parser = argparse.ArgumentParser(description="EuroPython Discord Bot") + parser.add_argument("--config-file", type=Path, required=True, help="Configuration file") + args = parser.parse_args() + + if "DISCORD_BOT_TOKEN" not in os.environ: + raise RuntimeError("Missing environment variable 'DISCORD_BOT_TOKEN'") + bot_auth_token = os.environ["DISCORD_BOT_TOKEN"] + + config_file_content = args.config_file.read_text() + config = Config(**tomllib.loads(config_file_content)) + + logging.basicConfig( + level=config.log_level, + stream=sys.stdout, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) try: - asyncio.run(run_bot(bot)) + asyncio.run(run_bot(config, auth_token=bot_auth_token)) except KeyboardInterrupt: _logger.info("Received KeyboardInterrupt, exiting...") diff --git a/src/europython_discord/cogs/guild_statistics.py b/src/europython_discord/cogs/guild_statistics.py new file mode 100644 index 00000000..e6a84060 --- /dev/null +++ b/src/europython_discord/cogs/guild_statistics.py @@ -0,0 +1,83 @@ +"""Commands for organisers.""" + +from __future__ import annotations + +import logging + +from discord import Role +from discord.ext import commands +from discord.utils import get as discord_get +from pydantic import BaseModel + +_logger = logging.getLogger(__name__) + + +class GuildStatisticsConfig(BaseModel): + required_role: str + + +class GuildStatisticsCog(commands.Cog): + """A cog with commands for organisers.""" + + def __init__(self, bot: commands.Bot, config: GuildStatisticsConfig) -> None: + self._bot = bot + self._required_role_name = config.required_role + + @commands.command(name="participants") + async def list_participants(self, ctx: commands.Context) -> None: + """Get statistics about registered participants.""" + # count members and roles, sorted from highest to lowest role + role_counts: dict[str, int] = {} + for role in await self.get_ordered_roles(ctx): + role_counts[role.name] = 0 + for member in ctx.guild.members: + for role in member.roles: + role_counts[role.name] += 1 + + # send message + lines = [f"{ctx.author.mention} Participant Statistics:"] + for role_name, count in role_counts.items(): + lines.append(f"* {count} {role_name}") + await ctx.send(content="\n".join(lines), delete_after=5) + + async def cog_check(self, ctx: commands.Context) -> bool: + """Check if the requested command shall be executed.""" + # check if user has required role + required_role = discord_get(ctx.guild.roles, name=self._required_role_name) + if ctx.author.get_role(required_role.id) is None: + _logger.info( + "%s (%r) tried to run %r in %s but does not have the role %s", + ctx.author.display_name, + ctx.author.id, + ctx.command.name, + ctx.channel.name, + required_role.name, + ) + return False + + # check if only users with required role can see the channel + all_roles = await self.get_ordered_roles(ctx) + role_index = all_roles.index(required_role) + next_lower_role = all_roles[role_index + 1] + if ctx.channel.permissions_for(next_lower_role).view_channel: + _logger.info( + "%s (%r) tried to run %r in %s but the channel is visible to next lower role %s", + ctx.author.display_name, + ctx.author.id, + ctx.command.name, + ctx.channel.name, + next_lower_role.name, + ) + return False + + return True + + async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: + """Handle a command error raised in this class.""" + _logger.error( + "An error occurred while running command %r:", ctx.command.name, exc_info=error + ) + + @staticmethod + async def get_ordered_roles(ctx: commands.Context) -> list[Role]: + return sorted(ctx.guild.roles, key=lambda r: r.position, reverse=True) diff --git a/src/europython_discord/cogs/ping.py b/src/europython_discord/cogs/ping.py index cc10779a..837a74fa 100644 --- a/src/europython_discord/cogs/ping.py +++ b/src/europython_discord/cogs/ping.py @@ -2,10 +2,10 @@ from discord.ext import commands -_logger = logging.getLogger(f"bot.{__name__}") +_logger = logging.getLogger(__name__) -class Ping(commands.Cog): +class PingCog(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot: commands.Bot = bot _logger.info("Cog 'Ping' has been initialized") diff --git a/src/europython_discord/config.toml b/src/europython_discord/config.toml deleted file mode 100644 index 2ac71105..00000000 --- a/src/europython_discord/config.toml +++ /dev/null @@ -1,88 +0,0 @@ -[roles] -ORGANISERS = 1363473647997812778 -VOLUNTEERS = 1363473649490853889 -VOLUNTEERS_ONSITE = 1331995051093528645 -VOLUNTEERS_REMOTE = 1331995051093528644 -SPEAKERS = 1331995051093528643 -SPONSORS = 1331995051093528642 -PARTICIPANTS = 1331995051093528640 -PARTICIPANTS_ONSITE = 1331995051093528639 -PARTICIPANTS_REMOTE = 1331995051093528638 - -[ticket_to_role] -# onsite participants -"Business" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Personal" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Education" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Community Contributors" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Grant ticket" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -# remote participants -"Remote Participation Ticket" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -"Remote Grant ticket" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -"Remote Community Organiser" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -# sponsors -"Sponsor Conference Pass" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE", "SPONSORS"] -# speakers -"Presenter" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE", "SPEAKERS"] - -[additional_roles_by_variation] -"Volunteer" = ["VOLUNTEERS"] - -[registration] -REG_CHANNEL_ID = 1363473814029209811 -REG_HELP_CHANNEL_ID = 1363473815392354455 -REG_LOG_CHANNEL_ID = 1363473816461906007 -REGISTERED_LOG_FILE = "registered_log.txt" - -[pretix] -PRETIX_BASE_URL = "https://pretix.eu/api/v1/organizers/europython/events/ep2025" -PRETIX_CACHE_FILE = "pretix_cache.json" - -[logging] -LOG_LEVEL = "INFO" - -[program_notifications] -# UTC offset in hours (e.g. 2 for CEST) -timezone_offset = 2 -api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json" -schedule_cache_file = "schedule.json" -livestream_url_file = "livestreams.toml" - -# optional simulated start time for testing program notifications -# simulated_start_time = "2024-07-10T07:30:00" - -# optional fast mode for faster testing of program notifications -# will only take effect if simulated_start_time is set -# fast_mode = true - -[program_notifications.rooms.main_channel] -name = "Main Channel" -channel_id = "1363473705455325306" - -[program_notifications.rooms.forum_hall] -name = "Forum Hall" -channel_id = "1363473707091366068" - -[program_notifications.rooms.south_hall_2a] -name = "South Hall 2A" -channel_id = "1363473708223693012" - -[program_notifications.rooms.south_hall_2b] -name = "South Hall 2B" -channel_id = "1363473709377261830" - -[program_notifications.rooms.north_hall] -name = "North Hall" -channel_id = "1363473710983680121" - -[program_notifications.rooms.terrace_2a] -name = "Terrace 2A" -channel_id = "1363473712124526715" - -[program_notifications.rooms.terrace_2b] -name = "Terrace 2B" -channel_id = "1363473713609310268" - -[program_notifications.rooms.exhibit_hall] -name = "Exhibit Hall" -channel_id = "1363473714573873263" diff --git a/src/europython_discord/configuration.py b/src/europython_discord/configuration.py deleted file mode 100644 index 0e9bdb24..00000000 --- a/src/europython_discord/configuration.py +++ /dev/null @@ -1,127 +0,0 @@ -from __future__ import annotations - -import logging -import sys -import tomllib -from datetime import datetime, timedelta, timezone -from pathlib import Path - -_logger = logging.getLogger(f"bot.{__name__}") - - -class Singleton(type): - _instances = {} # noqa: RUF012 (missing type annotation as typing.ClassVar) - - def __call__(cls, *args, **kwargs) -> Singleton: # noqa: ANN002,ANN003 (missing annotations) - if cls not in cls._instances: - cls._instances[cls] = super().__call__(*args, **kwargs) - return cls._instances[cls] - - -class Config(metaclass=Singleton): - _CONFIG_DEFAULT = "config.toml" - _CONFIG_LOCAL = "config.local.toml" - - def __init__(self) -> None: - # Configuration file - config = None - self.BASE_PATH = Path(__file__).resolve().parent - self.CONFIG_PATH = self._get_config_path(self.BASE_PATH) - with self.CONFIG_PATH.open("rb") as f: - config = tomllib.load(f) - - if not config: - _logger.critical("Error: Failed to load the config file at '%s'", self.CONFIG_PATH) - sys.exit(-1) - - try: - # Registration - self.REG_CHANNEL_ID = int(config["registration"]["REG_CHANNEL_ID"]) - self.REG_HELP_CHANNEL_ID = int(config["registration"]["REG_HELP_CHANNEL_ID"]) - self.REG_LOG_CHANNEL_ID = int(config["registration"]["REG_LOG_CHANNEL_ID"]) - self.REGISTERED_LOG_FILE = Path(config["registration"]["REGISTERED_LOG_FILE"]) - - # Pretix - self.PRETIX_BASE_URL = config["pretix"]["PRETIX_BASE_URL"] - self.PRETIX_CACHE_FILE = Path(config["pretix"]["PRETIX_CACHE_FILE"]) - - role_name_to_id: dict[str, int] = config["roles"] - self.ITEM_TO_ROLES: dict[str, list[int]] = self._translate_role_names_to_ids( - config["ticket_to_role"], role_name_to_id - ) - self.VARIATION_TO_ROLES: dict[str, list[int]] = self._translate_role_names_to_ids( - config["additional_roles_by_variation"], role_name_to_id - ) - - # Program Notifications - self.PROGRAM_API_URL: str = config["program_notifications"]["api_url"] - self.TIMEZONE_OFFSET: int = config["program_notifications"]["timezone_offset"] - self.SCHEDULE_CACHE_FILE = Path(config["program_notifications"]["schedule_cache_file"]) - self.LIVESTREAM_URL_FILE = Path(config["program_notifications"]["livestream_url_file"]) - - # like {'forum_hall': {'name': 'Forum Hall', 'channel_id': '123456'}} - self.PROGRAM_CHANNELS: dict[str, dict[str, str]] = { - room: {"name": details["name"], "channel_id": details["channel_id"]} - for room, details in config["program_notifications"]["rooms"].items() - } - - # optional testing parameters for program notifications - if simulated_start_time := config["program_notifications"].get("simulated_start_time"): - self.SIMULATED_START_TIME = datetime.fromisoformat(simulated_start_time).replace( - tzinfo=timezone(timedelta(hours=self.TIMEZONE_OFFSET)) - ) - else: - self.SIMULATED_START_TIME = None - - self.FAST_MODE: bool = config["program_notifications"].get("fast_mode", False) - - # Logging - self.LOG_LEVEL = config.get("logging", {}).get("LOG_LEVEL", "INFO") - - except KeyError: - _logger.exception( - "Error encountered while reading '%s'. Ensure that it contains the necessary" - " configuration fields. If you are using a local override of the main configuration" - " file, please compare the fields in it against the main `config.toml` file.", - self.CONFIG_PATH, - ) - sys.exit(-1) - - @staticmethod - def _translate_role_names_to_ids( - mapping: dict[str, list[str]], role_ids_by_name: dict[str, int] - ) -> dict[str, list[int]]: - """Parse the ticket mapping from role names to role ids.""" - ticket_to_role_ids = {} - - for ticket_type, roles in mapping.items(): - roles_ids = [role_ids_by_name[role] for role in roles] - ticket_to_role_ids[ticket_type] = roles_ids - - return ticket_to_role_ids - - def _get_config_path(self, base_path: Path) -> Path: - """Get the path to the relevant configuration file. - - To make local development easier, the committed configuration - file used for production can be overridden by a local config - file: If a local configuration file is present, it is used - instead of the default configuration file. - - Note that the files are not merged: All keys need to be present - in the local configuration file. One way of achieving this is to - make a copy of the committed config file and editing the value - you want to edit. - - The local config file is added to the `.gitignore`, which means - is safe to create the file without having to worry about - accidentally committing development configurations. - - :param base_path: The parent directory of the configuration file - :return: A path to a configuration file. Note that this path is - not guaranteed to exist: If the default configuration file is - deleted and there is no local configuration file, the path - points to a non-existing file - """ - local_config = base_path / self._CONFIG_LOCAL - return local_config if local_config.is_file() else base_path / self._CONFIG_DEFAULT diff --git a/src/europython_discord/extensions/__init__.py b/src/europython_discord/extensions/__init__.py deleted file mode 100644 index cfc952bc..00000000 --- a/src/europython_discord/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Extensions for the EuroPython Discord bot.""" diff --git a/src/europython_discord/extensions/organisers/__init__.py b/src/europython_discord/extensions/organisers/__init__.py deleted file mode 100644 index c4394f06..00000000 --- a/src/europython_discord/extensions/organisers/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Extension for tools for organisers.""" - -import tomllib - -from discord.ext import commands - -from europython_discord import configuration -from europython_discord.extensions.organisers import organisers, roles - - -async def setup(bot: commands.Bot) -> None: - """Set up the organisers extension.""" - config = configuration.Config() - with config.CONFIG_PATH.open("rb") as config_file: - raw_roles = tomllib.load(config_file)["roles"] - - roles_instance = roles.Roles(**{name.lower(): role_id for name, role_id in raw_roles.items()}) - await bot.add_cog(organisers.Organisers(bot=bot, roles=roles_instance)) diff --git a/src/europython_discord/extensions/organisers/organisers.py b/src/europython_discord/extensions/organisers/organisers.py deleted file mode 100644 index ba95eaed..00000000 --- a/src/europython_discord/extensions/organisers/organisers.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Commands for organisers.""" - -import dataclasses -import logging - -import discord -from discord.ext import commands - -from europython_discord.extensions.organisers.roles import Roles - -_logger = logging.getLogger(f"bot.{__name__}") - - -class Organisers(commands.Cog): - """A cog with commands for organisers.""" - - def __init__(self, bot: commands.Bot, roles: Roles) -> None: - self._bot = bot - self._roles = roles - - @commands.command(name="participants") - async def participants(self, ctx: commands.Context) -> None: - """Get statistics about registered participants.""" - embed = discord.Embed( - title="Participant Statistics", - colour=16747421, - ) - counts = self._get_counts(ctx.guild) - embed.add_field(name="Members (total)", value=counts.everyone, inline=False) - embed.add_field(name="Unregistered", value=counts.not_registered, inline=False) - embed.add_field(name="Participants", value=counts.participants, inline=False) - embed.add_field(name="Onsite Participants", value=counts.participants_onsite, inline=False) - embed.add_field(name="Remote Participants", value=counts.participants_remote, inline=False) - embed.add_field(name="Sponsors", value=counts.sponsors, inline=False) - embed.add_field(name="Speakers", value=counts.speakers, inline=False) - embed.add_field(name="Volunteers", value=counts.volunteers, inline=False) - embed.add_field(name="Onsite Volunteers", value=counts.volunteers_onsite, inline=False) - embed.add_field(name="Remote Volunteers", value=counts.volunteers_remote, inline=False) - embed.add_field(name="Organisers", value=counts.organisers, inline=False) - await ctx.send(embed=embed) - - def _get_counts(self, guild: discord.Guild) -> "_RoleCount": - """Get counts of member types. - - :param guild: The guild instance providing the information - :return: Counts of different roles and pseudo-roles - """ - return _RoleCount( - everyone=guild.member_count, - not_registered=sum(len(m.roles) == 1 for m in guild.members), - **{ - role: len(guild.get_role(role_id).members) - for role, role_id in dataclasses.asdict(self._roles).items() - }, - ) - - async def cog_check(self, ctx: commands.Context) -> bool: - """Check if the message author has the organisers role.""" - return any(role.id == self._roles.organisers for role in ctx.author.roles) - - async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: - """Handle a command error raised in this class.""" - if isinstance(error, commands.CheckFailure): - _logger.info( - "%s (%r) tried to run %r but did not pass the check!", - ctx.author.display_name, - ctx.author.id, - ctx.command.name, - ) - return - _logger.error( - "An error occurred while running command %r:", ctx.command.name, exc_info=error - ) - - def __hash__(self) -> int: - """Return the hash of this Cog.""" - return hash(id(self)) - - -@dataclasses.dataclass(frozen=True) -class _RoleCount: - """Counts of members.""" - - everyone: int - not_registered: int - organisers: int - volunteers: int - volunteers_onsite: int - sponsors: int - speakers: int - volunteers_remote: int - participants: int - participants_onsite: int - participants_remote: int diff --git a/src/europython_discord/extensions/organisers/roles.py b/src/europython_discord/extensions/organisers/roles.py deleted file mode 100644 index 49135e86..00000000 --- a/src/europython_discord/extensions/organisers/roles.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Data structure for role IDs.""" - -import dataclasses - - -@dataclasses.dataclass -class Roles: - """Role mapping for the organisers extension.""" - - organisers: int - volunteers: int - volunteers_onsite: int - volunteers_remote: int - sponsors: int - speakers: int - participants: int - participants_onsite: int - participants_remote: int diff --git a/src/europython_discord/program_notifications/cog.py b/src/europython_discord/program_notifications/cog.py index d49b37c8..306e27cf 100644 --- a/src/europython_discord/program_notifications/cog.py +++ b/src/europython_discord/program_notifications/cog.py @@ -1,42 +1,42 @@ import logging -from discord import Client, Embed, TextChannel +from discord import Client, TextChannel from discord.ext import commands, tasks +from discord.utils import get as discord_get -from europython_discord.configuration import Config from europython_discord.program_notifications import session_to_embed +from europython_discord.program_notifications.config import ProgramNotificationsConfig from europython_discord.program_notifications.livestream_connector import LivestreamConnector -from europython_discord.program_notifications.models import Session from europython_discord.program_notifications.program_connector import ProgramConnector -config = Config() -_logger = logging.getLogger(f"bot.{__name__}") +_logger = logging.getLogger(__name__) class ProgramNotificationsCog(commands.Cog): - def __init__(self, bot: Client) -> None: + def __init__(self, bot: Client, config: ProgramNotificationsConfig) -> None: self.bot = bot + self.config = config self.program_connector = ProgramConnector( - api_url=config.PROGRAM_API_URL, - timezone_offset=config.TIMEZONE_OFFSET, - cache_file=config.SCHEDULE_CACHE_FILE, - simulated_start_time=config.SIMULATED_START_TIME, - fast_mode=config.FAST_MODE, + api_url=self.config.api_url, + timezone_offset=self.config.timezone_offset, + cache_file=self.config.schedule_cache_file, + simulated_start_time=self.config.simulated_start_time, + fast_mode=self.config.fast_mode, ) - self.livestream_connector = LivestreamConnector(config.LIVESTREAM_URL_FILE) + self.livestream_connector = LivestreamConnector(self.config.livestream_url_file) self.notified_sessions = set() _logger.info("Cog 'Program Notifications' has been initialized") @commands.Cog.listener() async def on_ready(self) -> None: - if config.SIMULATED_START_TIME: + if self.config.simulated_start_time: _logger.info("Running in simulated time mode.") _logger.info("Will purge all room channels to avoid pile-up of test notifications.") await self.purge_all_room_channels() - _logger.debug(f"Simulated start time: {config.SIMULATED_START_TIME}") - _logger.debug(f"Fast mode: {config.FAST_MODE}") + _logger.debug(f"Simulated start time: {self.config.simulated_start_time}") + _logger.debug(f"Fast mode: {self.config.fast_mode}") _logger.info("Starting the session notifier...") self.notify_sessions.start() _logger.info("Cog 'Program Notifications' is ready") @@ -49,7 +49,7 @@ async def cog_load(self) -> None: self.fetch_schedule.start() self.fetch_livestreams.start() self.notify_sessions.change_interval( - seconds=2 if config.FAST_MODE and config.SIMULATED_START_TIME else 60 + seconds=2 if self.config.fast_mode and self.config.simulated_start_time else 60 ) _logger.info("Schedule updater started and interval set for the session notifier") @@ -71,74 +71,60 @@ async def fetch_livestreams(self) -> None: await self.livestream_connector.fetch_livestreams() _logger.info("Finished the periodic livestream update.") - async def set_room_topic(self, room: str, topic: str) -> None: - """Set the topic of a room channel.""" - room_channel = self._get_channel(room) - if room_channel is not None: - await room_channel.edit(topic=topic) - - async def notify_room(self, room: str, embed: Embed, content: str | None = None) -> None: - """Send the given notification to the room channel.""" - room_channel = self._get_channel(room) - if room_channel is not None: - await room_channel.send(content=content, embed=embed) - - def _get_channel(self, room: str) -> TextChannel | None: - room_key = room.lower().replace(" ", "_") - room_channel_config = config.PROGRAM_CHANNELS.get(room_key) - - if room_channel_config is None: - # this may be intended: in 2024, there were no dedicated channels for the tutorial rooms - _logger.warning(f"Cannot find configuration for room {room!r} (key: {room_key!r})") - return None - - channel_id = room_channel_config["channel_id"] - return self.bot.get_channel(int(channel_id)) - @tasks.loop() async def notify_sessions(self) -> None: - sessions: list[Session] = await self.program_connector.get_upcoming_sessions() - sessions_to_notify = [ - session for session in sessions if session not in self.notified_sessions - ] - first_message = True - - for session in sessions_to_notify: + # determine sessions to send notifications for + sessions_to_notify = [] + for session in await self.program_connector.get_upcoming_sessions(): + if session in self.notified_sessions: + continue # already notified if len(session.rooms) > 1: - continue # Don't notify registration sessions + continue # announcement or coffee/lunch break + sessions_to_notify.append(session) - livestream_url = await self.livestream_connector.get_livestream_url( - session.rooms[0], session.start.date() - ) + if not sessions_to_notify: + return - # Set the channel topic - await self.set_room_topic( - session.rooms[0], - f"Livestream: [YouTube]({livestream_url})" if livestream_url else "", - ) + main_notification_channel = discord_get( + self.bot.get_all_channels(), name=self.config.main_notification_channel_name + ) + await main_notification_channel.send(content="# Sessions starting in 5 minutes:") - embed = session_to_embed.create_session_embed(session, livestream_url) + for session in sessions_to_notify: + room_name = session.rooms[0] + room_channel = self._get_room_channel(room_name) - # # Notify specific rooms - # for room in session.rooms: - await self.notify_room( - session.rooms[0], embed, content=f"# Starting in 5 minutes @ {session.rooms[0]}" + # update room's livestream URL + livestream_url = await self.livestream_connector.get_livestream_url( + room_name, session.start.date() ) + embed = session_to_embed.create_session_embed(session, livestream_url) - # Prefix the first message to the main channel with a header - if first_message: - await self.notify_room( - "Main Channel", embed, content="# Sessions starting in 5 minutes:" + await main_notification_channel.send(embed=embed) + if room_channel is not None: + await room_channel.edit( + topic=f"Livestream: [YouTube]({livestream_url})" if livestream_url else "" + ) + await room_channel.send( + content=f"# Starting in 5 minutes @ {session.rooms[0]}", + embed=embed, ) - first_message = False - else: - await self.notify_room("Main Channel", embed) + + # send session notification message to room and main channel self.notified_sessions.add(session) async def purge_all_room_channels(self) -> None: _logger.info("Purging all room channels...") - for room in config.PROGRAM_CHANNELS.values(): - channel = self.bot.get_channel(int(room["channel_id"])) + for channel_name in self.config.rooms_to_channel_names.values(): + channel = discord_get(self.bot.get_all_channels(), name=channel_name) await channel.purge() - _logger.info("Purged all room channels channels.") + _logger.info("Purged all room channels.") + + def _get_room_channel(self, room_name: str) -> TextChannel | None: + channel_name = self.config.rooms_to_channel_names.get(room_name) + if channel_name is None: + _logger.warning(f"No notification channel configured for room {room_name!r}") + return None + + return discord_get(self.bot.get_all_channels(), name=channel_name) diff --git a/src/europython_discord/program_notifications/config.py b/src/europython_discord/program_notifications/config.py new file mode 100644 index 00000000..920d2e60 --- /dev/null +++ b/src/europython_discord/program_notifications/config.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from collections.abc import Mapping +from datetime import datetime +from pathlib import Path + +from pydantic import BaseModel + + +class ProgramNotificationsConfig(BaseModel): + timezone_offset: int + api_url: str + schedule_cache_file: Path + livestream_url_file: Path + main_notification_channel_name: str + rooms_to_channel_names: Mapping[str, str] + + simulated_start_time: datetime | None = None + fast_mode: bool = False diff --git a/src/europython_discord/program_notifications/program_connector.py b/src/europython_discord/program_notifications/program_connector.py index c6533863..d4447c0f 100644 --- a/src/europython_discord/program_notifications/program_connector.py +++ b/src/europython_discord/program_notifications/program_connector.py @@ -9,7 +9,7 @@ from europython_discord.program_notifications.models import Break, Schedule, Session -_logger = logging.getLogger(f"bot.{__name__}") +_logger = logging.getLogger(__name__) class ProgramConnector: diff --git a/src/europython_discord/registration/cog.py b/src/europython_discord/registration/cog.py index 0de58ba2..7169a94d 100644 --- a/src/europython_discord/registration/cog.py +++ b/src/europython_discord/registration/cog.py @@ -7,31 +7,30 @@ import discord from discord import Client, Forbidden, Interaction, Role from discord.ext import commands, tasks +from discord.utils import get as discord_get -from europython_discord.configuration import Config +from europython_discord.registration.config import RegistrationConfig from europython_discord.registration.pretix_connector import PretixConnector from europython_discord.registration.registration_logger import RegistrationLogger -config = Config() +_logger = logging.getLogger(__name__) -_logger = logging.getLogger(f"bot.{__name__}") +# Discord's colon-syntax `:point_left:` does not work in button labels, so we use `\N{...}` here +REGISTRATION_BUTTON_LABEL = "Register here \N{WHITE LEFT POINTING BACKHAND INDEX}" +WELCOME_MESSAGE_TITLE = "## Welcome to EuroPython 2025 on Discord! :tada::snake:" -class RegistrationButton(discord.ui.Button["Registration"]): - def __init__(self, parent_cog: RegistrationCog) -> None: - super().__init__() - self.parent_cog = parent_cog - self.label = "Register here 👈" - self.style = discord.ButtonStyle.green - - async def callback(self, interaction: discord.Interaction) -> None: - await interaction.response.send_modal(RegistrationForm(parent_cog=self.parent_cog)) - - -class RegistrationForm(discord.ui.Modal, title="EuroPython 2024 Registration"): - def __init__(self, parent_cog: RegistrationCog) -> None: +class RegistrationForm(discord.ui.Modal, title="EuroPython 2025 Registration"): + def __init__( + self, + config: RegistrationConfig, + pretix_connector: PretixConnector, + registration_logger: RegistrationLogger, + ) -> None: super().__init__() - self.parent_cog = parent_cog + self.config = config + self.pretix_connector = pretix_connector + self.registration_logger = registration_logger order_field = discord.ui.TextInput( label="Order ID (As printed on your badge or ticket)", @@ -56,7 +55,7 @@ async def on_submit(self, interaction: discord.Interaction) -> None: order = self.order_field.value _logger.debug(f"Registration attempt: {order=}, {name=}") - tickets = self.parent_cog.pretix_connector.get_tickets(order=order, name=name) + tickets = self.pretix_connector.get_tickets(order=order, name=name) if not tickets: await self.log_error_to_user( @@ -67,20 +66,20 @@ async def on_submit(self, interaction: discord.Interaction) -> None: _logger.info(f"No ticket found: {order=}, {name=}") return - if any(self.parent_cog.registration_logger.is_registered(ticket) for ticket in tickets): + if any(self.registration_logger.is_registered(ticket) for ticket in tickets): await self.log_error_to_user(interaction, "You have already registered.") await self.log_error_to_channel(interaction, f"Already registered: {order=}, {name=}") _logger.info(f"Already registered: {tickets}") return - role_ids = set() + role_names = set() for ticket in tickets: - if ticket.type in config.ITEM_TO_ROLES: - role_ids.update(config.ITEM_TO_ROLES[ticket.type]) - if ticket.variation in config.VARIATION_TO_ROLES: - role_ids.update(config.VARIATION_TO_ROLES[ticket.variation]) + if ticket.type in self.config.item_to_roles: + role_names.update(self.config.item_to_roles[ticket.type]) + if ticket.variation in self.config.variation_to_roles: + role_names.update(self.config.variation_to_roles[ticket.variation]) - if not role_ids: + if not role_names: await self.log_error_to_user(interaction, "No ticket found.") await self.log_error_to_channel(interaction, f"Tickets without roles: {tickets}") _logger.info(f"Tickets without role assignments: {tickets}") @@ -90,14 +89,14 @@ async def on_submit(self, interaction: discord.Interaction) -> None: _logger.info("Assigning nickname %r", nickname) await interaction.user.edit(nick=nickname) - roles = [discord.utils.get(interaction.guild.roles, id=role_id) for role_id in role_ids] - _logger.info("Assigning %r role_ids=%r", name, role_ids) + roles = [discord_get(interaction.guild.roles, name=role_name) for role_name in role_names] + _logger.info("Assigning %r role_names=%r", name, role_names) await interaction.user.add_roles(*roles) await self.log_registration_to_channel(interaction, name=name, order=order, roles=roles) await self.log_registration_to_user(interaction, name=name) for ticket in tickets: - await self.parent_cog.registration_logger.mark_as_registered(ticket) + await self.registration_logger.mark_as_registered(ticket) _logger.info(f"Registration successful: {order=}, {name=}") async def on_error(self, interaction: Interaction, error: Exception) -> None: @@ -126,79 +125,95 @@ async def log_registration_to_user(interaction: Interaction, *, name: str) -> No delete_after=None, ) - @staticmethod async def log_registration_to_channel( - interaction: Interaction, *, name: str, order: str, roles: list[Role] + self, interaction: Interaction, *, name: str, order: str, roles: list[Role] ) -> None: - channel = interaction.client.get_channel(config.REG_LOG_CHANNEL_ID) + channel = discord_get( + interaction.client.get_all_channels(), name=self.config.registration_log_channel_name + ) message_lines = [ - f"✅ : **<@{interaction.user.id}> REGISTERED**", + f":white_check_mark: **{interaction.user.mention} REGISTERED**", f"{name=} {order=} roles={[role.name for role in roles]}", ] await channel.send(content="\n".join(message_lines)) - @staticmethod - async def log_error_to_user(interaction: Interaction, message: str) -> None: + async def log_error_to_user(self, interaction: Interaction, message: str) -> None: + reg_help_channel = discord_get( + interaction.guild.channels, name=self.config.registration_help_channel_name + ) await interaction.response.send_message( - f"{message} If you need help, please contact us in <#{config.REG_HELP_CHANNEL_ID}>.", + f"{message} If you need help, please contact us in {reg_help_channel.mention}.", ephemeral=True, delete_after=None, ) - @staticmethod - async def log_error_to_channel(interaction: Interaction, message: str) -> None: - channel = interaction.client.get_channel(config.REG_LOG_CHANNEL_ID) - await channel.send(content=f"❌ : **<@{interaction.user.id}> ERROR**\n{message}") + async def log_error_to_channel(self, interaction: Interaction, message: str) -> None: + channel = discord_get( + interaction.client.get_all_channels(), name=self.config.registration_log_channel_name + ) + await channel.send(content=f":x: **{interaction.user.mention} ERROR**\n{message}") class RegistrationCog(commands.Cog): - def __init__(self, bot: Client) -> None: + def __init__(self, bot: Client, config: RegistrationConfig) -> None: self.bot = bot + self.config = config self.pretix_connector = PretixConnector( - url=config.PRETIX_BASE_URL, + url=self.config.pretix_base_url, token=os.environ["PRETIX_TOKEN"], - cache_file=config.PRETIX_CACHE_FILE, + cache_file=self.config.pretix_cache_file, ) - self.registration_logger = RegistrationLogger(config.REGISTERED_LOG_FILE) + self.registration_logger = RegistrationLogger(self.config.registered_cache_file) _logger.info("Cog 'Registration' has been initialized") @commands.Cog.listener() async def on_ready(self) -> None: - reg_channel = self.bot.get_channel(config.REG_CHANNEL_ID) - - await reg_channel.purge() await self.pretix_connector.fetch_pretix_data() + button = discord.ui.Button(style=discord.ButtonStyle.green, label=REGISTRATION_BUTTON_LABEL) + button.callback = lambda interaction: interaction.response.send_modal( + RegistrationForm( + config=self.config, + pretix_connector=self.pretix_connector, + registration_logger=self.registration_logger, + ) + ) view = discord.ui.View(timeout=None) # timeout=None to make it persistent - view.add_item(RegistrationButton(parent_cog=self)) + view.add_item(button) - welcome_message = create_welcome_message( - textwrap.dedent( - f""" - Follow these steps to complete your registration: + reg_help_channel = discord_get( + self.bot.get_all_channels(), name=self.config.registration_help_channel_name + ) + welcome_message = textwrap.dedent( + f""" + {WELCOME_MESSAGE_TITLE}\n + Follow these steps to complete your registration: - 1️⃣ Click on the green "Register here 👈" button below. + :one: Click on the green "{REGISTRATION_BUTTON_LABEL}" button below. - 2️⃣ Fill in your Order ID and the name on your ticket. You can find them - * Printed on your ticket - * Printed on your badge - * In the email "[EuroPython 2024] Your order: XXXXX" from support@pretix.eu + :two: Fill in your Order ID and the name on your ticket. You can find them + * Printed on your ticket + * Printed on your badge + * In the email "[EuroPython 2025] Your order: XXXXX" from support@pretix.eu - 3️⃣ Click "Submit". + :three: Click "Submit". - These steps will assign the correct server permissions and set your server nickname. + These steps will assign the correct server permissions and set your server nickname. - Experiencing trouble? Please contact us - * In the <#{config.REG_HELP_CHANNEL_ID}> channel - * By speaking to a volunteer in a yellow t-shirt + Experiencing trouble? Please contact us + * In the {reg_help_channel.mention} channel + * By speaking to a volunteer in a yellow t-shirt - Enjoy our EuroPython 2024 Community Server! 🐍💻🎉 - """ - ) + Enjoy our EuroPython 2025 Community Server! :snake::computer::tada: + """ ) - await reg_channel.send(embed=welcome_message, view=view) + channel = discord_get( + self.bot.get_all_channels(), name=self.config.registration_form_channel_name + ) + await channel.purge() + await channel.send(welcome_message, view=view) async def cog_load(self) -> None: """Load the initial schedule.""" @@ -211,13 +226,14 @@ async def cog_unload(self) -> None: self.fetch_pretix_updates.cancel() _logger.info("Replacing registration form with 'currently offline' message") - reg_channel = self.bot.get_channel(config.REG_CHANNEL_ID) + reg_channel = discord_get( + self.bot.get_all_channels(), name=self.config.registration_form_channel_name + ) await reg_channel.purge() await reg_channel.send( - embed=create_welcome_message( - "The registration bot is currently offline. " - "We apologize for the inconvenience and are working hard to fix the issue." - ) + f"{WELCOME_MESSAGE_TITLE}\n" + "The registration bot is currently offline. " + "We apologize for the inconvenience and are working hard to fix the issue." ) @tasks.loop(minutes=5) @@ -228,12 +244,3 @@ async def fetch_pretix_updates(self) -> None: _logger.info("Finished the periodic pretix update.") except Exception: _logger.exception("Periodic pretix update failed") - - -def create_welcome_message(body: str) -> discord.Embed: - orange = 0xFF8331 - return discord.Embed( - title="Welcome to EuroPython 2024 on Discord! 🎉🐍", - description=body, - color=orange, - ) diff --git a/src/europython_discord/registration/config.py b/src/europython_discord/registration/config.py new file mode 100644 index 00000000..b1dd1301 --- /dev/null +++ b/src/europython_discord/registration/config.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from pathlib import Path + +from pydantic import BaseModel + + +class RegistrationConfig(BaseModel): + # discord + registration_form_channel_name: str + registration_help_channel_name: str + registration_log_channel_name: str + + # pretix + pretix_base_url: str + item_to_roles: Mapping[str, Sequence[str]] + variation_to_roles: Mapping[str, Sequence[str]] + + # cache files + pretix_cache_file: Path + registered_cache_file: Path diff --git a/src/europython_discord/registration/pretix_connector.py b/src/europython_discord/registration/pretix_connector.py index 14afd4cf..29da2f8e 100644 --- a/src/europython_discord/registration/pretix_connector.py +++ b/src/europython_discord/registration/pretix_connector.py @@ -15,7 +15,7 @@ from europython_discord.registration.pretix_api_response_models import PretixItem, PretixOrder from europython_discord.registration.ticket import Ticket, generate_ticket_key -_logger = logging.getLogger(f"bot.{__name__}") +_logger = logging.getLogger(__name__) class PretixCache(BaseModel): diff --git a/src/europython_discord/registration/registration_logger.py b/src/europython_discord/registration/registration_logger.py index fcf8c1bb..1c7d55aa 100644 --- a/src/europython_discord/registration/registration_logger.py +++ b/src/europython_discord/registration/registration_logger.py @@ -6,7 +6,7 @@ from europython_discord.registration.ticket import Ticket -_logger = logging.getLogger(f"bot.{__name__}") +_logger = logging.getLogger(__name__) class RegistrationLogger: diff --git a/src/europython_discord/staging-config.toml b/src/europython_discord/staging-config.toml deleted file mode 100644 index 966877f0..00000000 --- a/src/europython_discord/staging-config.toml +++ /dev/null @@ -1,88 +0,0 @@ -[roles] -ORGANISERS = 1360676366244384878 -VOLUNTEERS = 1360676367561265335 -VOLUNTEERS_ONSITE = 1360676369159291060 -VOLUNTEERS_REMOTE = 1360676369725653158 -SPEAKERS = 1360676371356975124 -SPONSORS = 1360676372263080090 -PARTICIPANTS = 1360676375182180596 -PARTICIPANTS_ONSITE = 1360676376197468231 -PARTICIPANTS_REMOTE = 1360676377338183840 - -[ticket_to_role] -# onsite participants -"Business" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Personal" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Education" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Community Contributors" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -"Grant ticket" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE"] -# remote participants -"Remote Participation Ticket" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -"Remote Grant ticket" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -"Remote Community Organiser" = ["PARTICIPANTS", "PARTICIPANTS_REMOTE"] -# sponsors -"Sponsor Conference Pass" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE", "SPONSORS"] -# speakers -"Presenter" = ["PARTICIPANTS", "PARTICIPANTS_ONSITE", "SPEAKERS"] - -[additional_roles_by_variation] -"Volunteer" = ["VOLUNTEERS"] - -[registration] -REG_CHANNEL_ID = 1361000861106700510 -REG_HELP_CHANNEL_ID = 1360981983462686925 -REG_LOG_CHANNEL_ID = 1360981984972636438 -REGISTERED_LOG_FILE = "registered_log.txt" - -[pretix] -PRETIX_BASE_URL = "https://pretix.eu/api/v1/organizers/europython/events/ep2025-staging" -PRETIX_CACHE_FILE = "pretix_cache.json" - -[logging] -LOG_LEVEL = "INFO" - -[program_notifications] -# UTC offset in hours (e.g. 2 for CEST) -timezone_offset = 2 -api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json" -schedule_cache_file = "schedule.json" -livestream_url_file = "livestreams.toml" - -# optional simulated start time for testing program notifications -simulated_start_time = "2025-07-14T09:15:00" - -# optional fast mode for faster testing of program notifications -# will only take effect if simulated_start_time is set -fast_mode = true - -[program_notifications.rooms.main_channel] -name = "Main Channel" -channel_id = "1360981875421610084" - -[program_notifications.rooms.forum_hall] -name = "Forum Hall" -channel_id = "1360981877522956369" - -[program_notifications.rooms.south_hall_2a] -name = "South Hall 2A" -channel_id = "1360981879196487730" - -[program_notifications.rooms.south_hall_2b] -name = "South Hall 2B" -channel_id = "1360981880584929363" - -[program_notifications.rooms.north_hall] -name = "North Hall" -channel_id = "1360981881851478257" - -[program_notifications.rooms.terrace_2a] -name = "Terrace 2A" -channel_id = "1360981883411759235" - -[program_notifications.rooms.terrace_2b] -name = "Terrace 2B" -channel_id = "1360981884556808382" - -[program_notifications.rooms.exhibit_hall] -name = "Exhibit Hall" -channel_id = "1360981885798322288" diff --git a/test-config.toml b/test-config.toml new file mode 100644 index 00000000..4987d52e --- /dev/null +++ b/test-config.toml @@ -0,0 +1,57 @@ +log_level = "INFO" + +[registration] +registration_form_channel_name = "registration-form" +registration_help_channel_name = "registration-help" +registration_log_channel_name = "registration-log" + +pretix_base_url = "https://pretix.eu/api/v1/organizers/europython/events/ep2025" + +registered_cache_file = "registered_log.txt" +pretix_cache_file = "pretix_cache.json" + +[registration.item_to_roles] +# onsite participants +"Business" = ["Participants", "Onsite Participants"] +"Personal" = ["Participants", "Onsite Participants"] +"Education" = ["Participants", "Onsite Participants"] +"Community Contributors" = ["Participants", "Onsite Participants"] +"Grant ticket" = ["PARTICIPANTS", "Onsite Participants"] +# remote participants +"Remote Participation Ticket" = ["Participants", "Remote Participants"] +"Remote Grant ticket" = ["Participants", "Remote Participants"] +"Remote Community Organiser" = ["Participants", "Remote Participants"] +# sponsors +"Sponsor Conference Pass" = ["Participants", "Onsite Participants", "Sponsors"] +# speakers +"Presenter" = ["Participants", "Onsite Participants", "Speakers"] + +[registration.variation_to_roles] +"Volunteer" = ["Volunteers"] + +[program_notifications] +# UTC offset in hours (e.g. 2 for CEST) +timezone_offset = 2 +api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json" +schedule_cache_file = "schedule_cache.json" +livestream_url_file = "test-livestreams.toml" +main_notification_channel_name = "programme-notifications" + +# optional simulated start time for testing program notifications +simulated_start_time = "2025-07-14T09:15:00+02:00" + +# optional fast mode for faster testing of program notifications +# will only take effect if simulated_start_time is set +fast_mode = true + +[program_notifications.rooms_to_channel_names] +"Forum Hall" = "forum-hall" +"South Hall 2A" = "south-hall-2a" +"South Hall 2B" = "south-hall-2b" +"North Hall" = "north-hall" +"Terrace 2A" = "terrace-2a" +"Terrace 2B" = "terrace-2b" +"Exhibit Hall" = "exhibit-hall" + +[guild_statistics] +required_role = "Organizers" diff --git a/livestreams.toml b/test-livestreams.toml similarity index 100% rename from livestreams.toml rename to test-livestreams.toml diff --git a/uv.lock b/uv.lock index 346f540d..5bd95446 100644 --- a/uv.lock +++ b/uv.lock @@ -110,47 +110,6 @@ 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 = "ansible" -version = "11.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ansible-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/e3/0cca4bd57b2c5c39d403d369182080c5eecedeb58df74e39ea4a563ddff5/ansible-11.4.0.tar.gz", hash = "sha256:d25a7f26bf5821f8043bc806019822fd2810bd65e6b6bafb698bbeedadba72bf", size = 42407185, upload-time = "2025-03-25T19:16:11.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/d5/30374bb92397544d4ba1ca391234e66bfe495c0f094816dc17101c5e18a1/ansible-11.4.0-py3-none-any.whl", hash = "sha256:fb56f6e5d5b08f69499a76f0972ac0b88ddc488ada1f386129ba40cb0b5c6ec7", size = 54155465, upload-time = "2025-03-25T19:16:06.069Z" }, -] - -[[package]] -name = "ansible-core" -version = "2.18.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "resolvelib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9c/cc/ffab05d33cb327001cd5b48209cbf4312608b09c8604286eab3da1263912/ansible_core-2.18.4.tar.gz", hash = "sha256:e1f8f5c33546362b0ee933e0969a3ba364b486515a6fa1bc25ebb5d95f8ec5f4", size = 3081918, upload-time = "2025-03-25T18:12:33.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/6b/db3ad0f10cbb9abd0cc6d443488f0d6826ca0f1afbaf7e97adee09ff5432/ansible_core-2.18.4-py3-none-any.whl", hash = "sha256:c642d484c1d3486a923b152150034eddd5cdf4bea58039c928183900fb35d8ba", size = 2217181, upload-time = "2025-03-25T18:12:32.383Z" }, -] - -[[package]] -name = "arrow" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "types-python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, -] - [[package]] name = "attrs" version = "25.3.0" @@ -200,60 +159,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, ] -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -263,45 +168,6 @@ wheels = [ { 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]] -name = "cryptography" -version = "44.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807, upload-time = "2025-03-02T00:01:37.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361, upload-time = "2025-03-02T00:00:06.528Z" }, - { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350, upload-time = "2025-03-02T00:00:09.537Z" }, - { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572, upload-time = "2025-03-02T00:00:12.03Z" }, - { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124, upload-time = "2025-03-02T00:00:14.518Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122, upload-time = "2025-03-02T00:00:17.212Z" }, - { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831, upload-time = "2025-03-02T00:00:19.696Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583, upload-time = "2025-03-02T00:00:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753, upload-time = "2025-03-02T00:00:25.038Z" }, - { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550, upload-time = "2025-03-02T00:00:26.929Z" }, - { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367, upload-time = "2025-03-02T00:00:28.735Z" }, - { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843, upload-time = "2025-03-02T00:00:30.592Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057, upload-time = "2025-03-02T00:00:33.393Z" }, - { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789, upload-time = "2025-03-02T00:00:36.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919, upload-time = "2025-03-02T00:00:38.581Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812, upload-time = "2025-03-02T00:00:42.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571, upload-time = "2025-03-02T00:00:46.026Z" }, - { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832, upload-time = "2025-03-02T00:00:48.647Z" }, - { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719, upload-time = "2025-03-02T00:00:51.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852, upload-time = "2025-03-02T00:00:53.317Z" }, - { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906, upload-time = "2025-03-02T00:00:56.49Z" }, - { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572, upload-time = "2025-03-02T00:00:59.995Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631, upload-time = "2025-03-02T00:01:01.623Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792, upload-time = "2025-03-02T00:01:04.133Z" }, - { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513, upload-time = "2025-03-02T00:01:22.911Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432, upload-time = "2025-03-02T00:01:24.701Z" }, - { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421, upload-time = "2025-03-02T00:01:26.335Z" }, - { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081, upload-time = "2025-03-02T00:01:28.938Z" }, -] - [[package]] name = "discord-py" version = "2.5.2" @@ -322,18 +188,13 @@ source = { editable = "." } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, - { name = "arrow" }, - { name = "certifi" }, { name = "discord-py" }, { name = "pydantic" }, - { name = "python-dotenv" }, { name = "unidecode" }, - { name = "yarl" }, ] [package.dev-dependencies] dev = [ - { name = "ansible" }, { name = "pytest" }, { name = "pytest-aiohttp" }, { name = "pytest-asyncio" }, @@ -344,18 +205,13 @@ dev = [ requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiohttp", specifier = ">=3.11.16" }, - { name = "arrow", specifier = ">=1.3.0" }, - { name = "certifi", specifier = ">=2024.7.4" }, { name = "discord-py", specifier = ">=2.3.1" }, { name = "pydantic", specifier = ">=2.8.2" }, - { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "unidecode", specifier = ">=1.3.8" }, - { name = "yarl", specifier = ">=1.19.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "ansible", specifier = ">=10.2.0" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-aiohttp", specifier = ">=1.1.0" }, { name = "pytest-asyncio", specifier = ">=0.26.0" }, @@ -457,66 +313,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, -] - [[package]] name = "multidict" version = "6.4.3" @@ -685,15 +481,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, ] -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, -] - [[package]] name = "pydantic" version = "2.11.4" @@ -815,71 +602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "resolvelib" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, -] - [[package]] name = "ruff" version = "0.11.6" @@ -905,24 +627,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766, upload-time = "2025-04-17T13:35:52.014Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802, upload-time = "2024-12-06T02:56:41.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384, upload-time = "2024-12-06T02:56:39.412Z" }, -] - [[package]] name = "typing-extensions" version = "4.13.2"