diff options
| author | 2022-07-09 23:34:58 +0100 | |
|---|---|---|
| committer | 2022-07-09 23:34:58 +0100 | |
| commit | 280e749963f3e44dd4598616524706f5c445f6f2 (patch) | |
| tree | f3cff391f06795e243130dee1a6feaa300de3617 | |
| parent | Merge pull request #16 from python-discord/pin-d.py-version (diff) | |
| parent | Add concurrency rules to both workflows (diff) | |
Merge pull request #18 from python-discord/botcore-migration
| -rw-r--r-- | .github/workflows/deploy.yml | 4 | ||||
| -rw-r--r-- | .github/workflows/lint.yml | 18 | ||||
| -rw-r--r-- | metricity/__init__.py | 18 | ||||
| -rw-r--r-- | metricity/__main__.py | 44 | ||||
| -rw-r--r-- | metricity/bot.py | 438 | ||||
| -rw-r--r-- | metricity/database.py | 36 | ||||
| -rw-r--r-- | metricity/exts/__init__.py | 1 | ||||
| -rw-r--r-- | metricity/exts/error_handler.py | 42 | ||||
| -rw-r--r-- | metricity/exts/event_listeners/__init__.py | 1 | ||||
| -rw-r--r-- | metricity/exts/event_listeners/_utils.py | 36 | ||||
| -rw-r--r-- | metricity/exts/event_listeners/guild_listeners.py | 187 | ||||
| -rw-r--r-- | metricity/exts/event_listeners/member_listeners.py | 124 | ||||
| -rw-r--r-- | metricity/exts/event_listeners/message_listeners.py | 62 | ||||
| -rw-r--r-- | metricity/exts/status.py | 60 | ||||
| -rw-r--r-- | metricity/models.py | 3 | ||||
| -rw-r--r-- | metricity/utils.py | 35 | ||||
| -rw-r--r-- | poetry.lock | 598 | ||||
| -rw-r--r-- | pyproject.toml | 34 | ||||
| -rw-r--r-- | tox.ini | 8 | 
19 files changed, 1068 insertions, 681 deletions
| diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 65f9d85..dbb3d4d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,10 @@ on:      branches:        - main +concurrency: +  group: ${{ github.workflow }}-${{ github.ref }} +  cancel-in-progress: true +  jobs:    push_docker_image:      name: Build & Publish Docker image diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f21e65..65c855e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,10 @@ on:    pull_request:      branches: [ main ] +concurrency: +  group: ${{ github.workflow }}-${{ github.ref }} +  cancel-in-progress: true +  jobs:    lint:      name: "Lint code" @@ -18,14 +22,12 @@ jobs:      - name: Checkout branch        uses: actions/checkout@v2 -    - name: Setup Python -      uses: actions/setup-python@v2 - -    - name: Install Poetry and project dependencies -      uses: knowsuchagency/poetry-install@v2 - -    - name: Setup flake8 annotations -      uses: rbialon/flake8-annotations@v1 +    - name: Install Python Dependencies +      uses: HassanAbouelela/actions/setup-python@setup-python_v1.1.0 +      with: +        # Set dev=true to install flake8 extensions, which are dev dependencies +        dev: true +        python_version: 3.9      - name: Lint code with Flake8        run: poetry run flake8 . --count --show-source --statistics diff --git a/metricity/__init__.py b/metricity/__init__.py index 5fecffc..9216f05 100644 --- a/metricity/__init__.py +++ b/metricity/__init__.py @@ -1,12 +1,20 @@  """Metric collection for the Python Discord server.""" +import asyncio  import logging +import os +from typing import TYPE_CHECKING +  import coloredlogs +from botcore.utils import apply_monkey_patches  from metricity.config import PythonConfig -__version__ = "1.3.0" +if TYPE_CHECKING: +    from metricity.bot import Bot + +__version__ = "1.4.0"  # Set root log level  logging.basicConfig(level=PythonConfig.log_level) @@ -18,3 +26,11 @@ logging.getLogger("discord.client").setLevel(PythonConfig.discord_log_level)  # Gino has an obnoxiously loud log for all queries executed, not great when inserting  # tens of thousands of users, so we can disable that (it's just a SQLAlchemy logger)  logging.getLogger("gino.engine._SAEngine").setLevel(logging.WARNING) + +# On Windows, the selector event loop is required for aiodns. +if os.name == "nt": +    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +apply_monkey_patches() + +instance: "Bot" = None  # Global Bot instance. diff --git a/metricity/__main__.py b/metricity/__main__.py index bc711b3..71fe8a7 100644 --- a/metricity/__main__.py +++ b/metricity/__main__.py @@ -1,9 +1,49 @@  """Entry point for the Metricity application.""" -from metricity.bot import bot +import asyncio + +import aiohttp +import discord +from discord.ext import commands + +import metricity +from metricity.bot import Bot  from metricity.config import BotConfig +async def main() -> None: +    """Entry async method for starting the bot.""" +    intents = discord.Intents( +        guilds=True, +        members=True, +        bans=False, +        emojis=False, +        integrations=False, +        webhooks=False, +        invites=False, +        voice_states=False, +        presences=False, +        messages=True, +        reactions=False, +        typing=False +    ) + +    async with aiohttp.ClientSession() as session: +        metricity.instance = Bot( +            guild_id=BotConfig.guild_id, +            http_session=session, +            command_prefix=commands.when_mentioned, +            activity=discord.Game(f"Metricity {metricity.__version__}"), +            intents=intents, +            max_messages=None, +            allowed_mentions=None, +            allowed_roles=None, +            help_command=None, +        ) +        async with metricity.instance as _bot: +            await _bot.start(BotConfig.token) + +  def start() -> None:      """Start the Metricity application.""" -    bot.run(BotConfig.token) +    asyncio.run(main()) diff --git a/metricity/bot.py b/metricity/bot.py index 73125bf..17e6a6d 100644 --- a/metricity/bot.py +++ b/metricity/bot.py @@ -1,430 +1,32 @@  """Creating and configuring a Discord client for Metricity."""  import asyncio -import logging -from typing import Any, Generator, List -from asyncpg.exceptions import UniqueViolationError -from discord import ( -    CategoryChannel, -    Game, -    Guild, -    Intents, -    Member, -    Message as DiscordMessage, -    MessageType, -    RawBulkMessageDeleteEvent, -    RawMessageDeleteEvent, -    Thread as ThreadChannel, -    VoiceChannel, -) -from discord.abc import Messageable -from discord.ext.commands import Bot +from botcore import BotBase +from botcore.utils import logging, scheduling -from metricity import __version__ -from metricity.config import BotConfig -from metricity.database import connect, db -from metricity.models import Category, Channel, Message, Thread, User +from metricity import exts +from metricity.database import connect -log = logging.getLogger(__name__) +log = logging.get_logger(__name__) -intents = Intents( -    guilds=True, -    members=True, -    bans=False, -    emojis=False, -    integrations=False, -    webhooks=False, -    invites=False, -    voice_states=False, -    presences=False, -    messages=True, -    reactions=False, -    typing=False -) +class Bot(BotBase): +    """A subclass of `botcore.BotBase` that implements bot-specific functions.""" -bot = Bot( -    command_prefix="", -    help_command=None, -    intents=intents, -    max_messages=None, -    activity=Game(f"Metricity {__version__}") -) +    def __init__(self, *args, **kwargs) -> None: +        super().__init__(*args, **kwargs) -sync_process_complete = asyncio.Event() -channel_sync_in_progress = asyncio.Event() -db_ready = asyncio.Event() +        self.sync_process_complete = asyncio.Event() +        self.channel_sync_in_progress = asyncio.Event() +    async def setup_hook(self) -> None: +        """Connect to db and load cogs.""" +        await super().setup_hook() +        log.info(f"Metricity is online, logged in as {self.user}") +        await connect() +        scheduling.create_task(self.load_extensions(exts)) -async def insert_thread(thread: ThreadChannel) -> None: -    """Insert the given thread to the database.""" -    await Thread.create( -        id=str(thread.id), -        parent_channel_id=str(thread.parent_id), -        name=thread.name, -        archived=thread.archived, -        auto_archive_duration=thread.auto_archive_duration, -        locked=thread.locked, -        type=thread.type.name, -    ) - - -async def sync_channels(guild: Guild) -> None: -    """Sync channels and categories with the database.""" -    channel_sync_in_progress.clear() - -    log.info("Beginning category synchronisation process") - -    for channel in guild.channels: -        if isinstance(channel, CategoryChannel): -            if db_cat := await Category.get(str(channel.id)): -                await db_cat.update(name=channel.name).apply() -            else: -                await Category.create(id=str(channel.id), name=channel.name) - -    log.info("Category synchronisation process complete, synchronising channels") - -    for channel in guild.channels: -        if channel.category: -            if channel.category.id in BotConfig.ignore_categories: -                continue - -        if ( -            not isinstance(channel, CategoryChannel) and -            not isinstance(channel, VoiceChannel) -        ): -            category_id = str(channel.category.id) if channel.category else None -            # Cast to bool so is_staff is False if channel.category is None -            is_staff = bool( -                channel.category -                and channel.category.id in BotConfig.staff_categories -            ) -            if db_chan := await Channel.get(str(channel.id)): -                await db_chan.update( -                    name=channel.name, -                    category_id=category_id, -                    is_staff=is_staff, -                ).apply() -            else: -                await Channel.create( -                    id=str(channel.id), -                    name=channel.name, -                    category_id=category_id, -                    is_staff=is_staff, -                ) - -    log.info("Channel synchronisation process complete, synchronising threads") - -    for thread in guild.threads: -        if thread.parent and thread.parent.category: -            if thread.parent.category.id in BotConfig.ignore_categories: -                continue -        else: -            # This is a forum channel, not currently supported by Discord.py. Ignore it. -            continue - -        if db_thread := await Thread.get(str(thread.id)): -            await db_thread.update( -                name=thread.name, -                archived=thread.archived, -                auto_archive_duration=thread.auto_archive_duration, -                locked=thread.locked, -                type=thread.type.name, -            ).apply() -        else: -            await insert_thread(thread) -    channel_sync_in_progress.set() - - -async def sync_thread_archive_state(guild: Guild) -> None: -    """Sync the archive state of all threads in the database with the state in guild.""" -    active_thread_ids = [str(thread.id) for thread in guild.threads] -    async with db.transaction() as tx: -        async for db_thread in tx.connection.iterate(Thread.query): -            await db_thread.update(archived=db_thread.id not in active_thread_ids).apply() - - -def gen_chunks( -    chunk_src: List[Any], -    chunk_size: int -) -> Generator[List[Any], None, List[Any]]: -    """Yield successive n-sized chunks from lst.""" -    for i in range(0, len(chunk_src), chunk_size): -        yield chunk_src[i:i + chunk_size] - - -async def on_ready() -> None: -    """Initiate tasks when the bot comes online.""" -    log.info(f"Metricity is online, logged in as {bot.user}") -    await connect() -    db_ready.set() - - -async def on_guild_channel_create(channel: Messageable) -> None: -    """Sync the channels when one is created.""" -    await db_ready.wait() - -    if channel.guild.id != BotConfig.guild_id: -        return - -    await sync_channels(channel.guild) - - -async def on_guild_channel_update(_before: Messageable, channel: Messageable) -> None: -    """Sync the channels when one is updated.""" -    await db_ready.wait() - -    if channel.guild.id != BotConfig.guild_id: -        return - -    await sync_channels(channel.guild) - - -async def on_thread_join(thread: ThreadChannel) -> None: -    """ -    Sync channels when thread join is triggered. - -    Unlike what the name suggested, this is also triggered when: -       - A thread is created. -       - An un-cached thread is un-archived. -    """ -    await db_ready.wait() - -    if thread.guild.id != BotConfig.guild_id: -        return - -    await sync_channels(thread.guild) - - -async def on_thread_update(_before: Messageable, thread: Messageable) -> None: -    """Sync the channels when one is updated.""" -    await db_ready.wait() - -    if thread.guild.id != BotConfig.guild_id: -        return - -    await sync_channels(thread.guild) - - -async def on_guild_available(guild: Guild) -> None: -    """Synchronize the user table with the Discord users.""" -    await db_ready.wait() - -    log.info(f"Received guild available for {guild.id}") - -    if guild.id != BotConfig.guild_id: -        return log.info("Guild was not the configured guild, discarding event") - -    await sync_channels(guild) - -    log.info("Beginning thread archive state synchronisation process") -    await sync_thread_archive_state(guild) - -    log.info("Beginning user synchronisation process") - -    await User.update.values(in_guild=False).gino.status() - -    users = [] - -    for user in guild.members: -        users.append({ -            "id": str(user.id), -            "name": user.name, -            "avatar_hash": getattr(user.avatar, "key", None), -            "guild_avatar_hash": getattr(user.guild_avatar, "key", None), -            "joined_at": user.joined_at, -            "created_at": user.created_at, -            "is_staff": BotConfig.staff_role_id in [role.id for role in user.roles], -            "bot": user.bot, -            "in_guild": True, -            "public_flags": dict(user.public_flags), -            "pending": user.pending -        }) - -    log.info(f"Performing bulk upsert of {len(users)} rows") - -    user_chunks = gen_chunks(users, 500) - -    for chunk in user_chunks: -        log.info(f"Upserting chunk of {len(chunk)}") -        await User.bulk_upsert(chunk) - -    log.info("User upsert complete") - -    sync_process_complete.set() - - -async def on_member_join(member: Member) -> None: -    """On a user joining the server add them to the database.""" -    await db_ready.wait() -    await sync_process_complete.wait() - -    if member.guild.id != BotConfig.guild_id: -        return - -    if db_user := await User.get(str(member.id)): -        await db_user.update( -            id=str(member.id), -            name=member.name, -            avatar_hash=getattr(member.avatar, "key", None), -            guild_avatar_hash=getattr(member.guild_avatar, "key", None), -            joined_at=member.joined_at, -            created_at=member.created_at, -            is_staff=BotConfig.staff_role_id in [role.id for role in member.roles], -            public_flags=dict(member.public_flags), -            pending=member.pending, -            in_guild=True -        ).apply() -    else: -        try: -            await User.create( -                id=str(member.id), -                name=member.name, -                avatar_hash=getattr(member.avatar, "key", None), -                guild_avatar_hash=getattr(member.guild_avatar, "key", None), -                joined_at=member.joined_at, -                created_at=member.created_at, -                is_staff=BotConfig.staff_role_id in [role.id for role in member.roles], -                public_flags=dict(member.public_flags), -                pending=member.pending, -                in_guild=True -            ) -        except UniqueViolationError: -            pass - - -async def on_member_remove(member: Member) -> None: -    """On a user leaving the server mark in_guild as False.""" -    await db_ready.wait() -    await sync_process_complete.wait() - -    if member.guild.id != BotConfig.guild_id: -        return - -    if db_user := await User.get(str(member.id)): -        await db_user.update( -            in_guild=False -        ).apply() - - -async def on_member_update(before: Member, member: Member) -> None: -    """When a member updates their profile, update the DB record.""" -    await sync_process_complete.wait() - -    if member.guild.id != BotConfig.guild_id: -        return - -    # Joined at will be null if we are not ready to process events yet -    if not member.joined_at: -        return - -    roles = set([role.id for role in member.roles]) - -    if db_user := await User.get(str(member.id)): -        if ( -            db_user.name != member.name or -            db_user.avatar_hash != getattr(member.avatar, "key", None) or -            db_user.guild_avatar_hash != getattr(member.guild_avatar, "key", None) or -            BotConfig.staff_role_id in -            [role.id for role in member.roles] != db_user.is_staff -            or db_user.pending is not member.pending -        ): -            await db_user.update( -                id=str(member.id), -                name=member.name, -                avatar_hash=getattr(member.avatar, "key", None), -                guild_avatar_hash=getattr(member.guild_avatar, "key", None), -                joined_at=member.joined_at, -                created_at=member.created_at, -                is_staff=BotConfig.staff_role_id in roles, -                public_flags=dict(member.public_flags), -                in_guild=True, -                pending=member.pending -            ).apply() -    else: -        try: -            await User.create( -                id=str(member.id), -                name=member.name, -                avatar_hash=getattr(member.avatar, "key", None), -                guild_avatar_hash=getattr(member.guild_avatar, "key", None), -                joined_at=member.joined_at, -                created_at=member.created_at, -                is_staff=BotConfig.staff_role_id in roles, -                public_flags=dict(member.public_flags), -                in_guild=True, -                pending=member.pending -            ) -        except UniqueViolationError: -            pass - - -async def on_message(message: DiscordMessage) -> None: -    """Add a message to the table when one is sent providing the author has accepted.""" -    await db_ready.wait() - -    if not message.guild: -        return - -    if message.author.bot: -        return - -    if message.guild.id != BotConfig.guild_id: -        return - -    if message.type == MessageType.thread_created: -        return - -    await sync_process_complete.wait() -    await channel_sync_in_progress.wait() - -    if not await User.get(str(message.author.id)): -        return - -    cat_id = message.channel.category.id if message.channel.category else None - -    if cat_id in BotConfig.ignore_categories: -        return - -    args = { -        "id": str(message.id), -        "channel_id": str(message.channel.id), -        "author_id": str(message.author.id), -        "created_at": message.created_at -    } - -    if isinstance(message.channel, ThreadChannel): -        if not message.channel.parent: -            # This is a forum channel, not currently supported by Discord.py. Ignore it. -            return -        thread = message.channel -        args["channel_id"] = str(thread.parent_id) -        args["thread_id"] = str(thread.id) - -    await Message.create(**args) - - -async def on_raw_message_delete(message: RawMessageDeleteEvent) -> None: -    """If a message is deleted and we have a record of it set the is_deleted flag.""" -    if message := await Message.get(str(message.message_id)): -        await message.update(is_deleted=True).apply() - - -async def on_raw_bulk_message_delete(messages: RawBulkMessageDeleteEvent) -> None: -    """If messages are deleted in bulk and we have a record of them set the is_deleted flag.""" -    for message_id in messages.message_ids: -        if message := await Message.get(str(message_id)): -            await message.update(is_deleted=True).apply() +    async def on_error(self, event: str, *args, **kwargs) -> None: +        """Log errors raised in event listeners rather than printing them to stderr.""" +        log.exception(f"Unhandled exception in {event}.") diff --git a/metricity/database.py b/metricity/database.py index 1534e2c..a4d953e 100644 --- a/metricity/database.py +++ b/metricity/database.py @@ -1,7 +1,11 @@ -"""Methods for connecting and interacting with the database.""" +"""General utility functions and classes for Metricity.""" +  import logging +from datetime import datetime, timezone  import gino +from sqlalchemy.engine import Dialect +from sqlalchemy.types import DateTime, TypeDecorator  from metricity.config import DatabaseConfig @@ -26,3 +30,33 @@ async def connect() -> None:      log.info("Initiating connection to the database")      await db.set_bind(build_db_uri())      log.info("Database connection established") + + +class TZDateTime(TypeDecorator): +    """ +    A db type that supports the use of aware datetimes in user-land. + +    Source from SQLAlchemy docs: +    https://docs.sqlalchemy.org/en/14/core/custom_types.html#store-timezone-aware-timestamps-as-timezone-naive-utc + +    Edited to include docstrings and type hints. +    """ + +    impl = DateTime +    cache_ok = True + +    def process_bind_param(self, value: datetime, dialect: Dialect) -> datetime: +        """Convert the value to aware before saving to db.""" +        if value is not None: +            if not value.tzinfo: +                raise TypeError("tzinfo is required") +            value = value.astimezone(timezone.utc).replace( +                tzinfo=None +            ) +        return value + +    def process_result_value(self, value: datetime, dialect: Dialect) -> datetime: +        """Convert the value to aware before passing back to user-land.""" +        if value is not None: +            value = value.replace(tzinfo=timezone.utc) +        return value diff --git a/metricity/exts/__init__.py b/metricity/exts/__init__.py new file mode 100644 index 0000000..a8cce86 --- /dev/null +++ b/metricity/exts/__init__.py @@ -0,0 +1 @@ +"""A module containing all extensions to be loaded into the bot on startup.""" diff --git a/metricity/exts/error_handler.py b/metricity/exts/error_handler.py new file mode 100644 index 0000000..0f35de2 --- /dev/null +++ b/metricity/exts/error_handler.py @@ -0,0 +1,42 @@ +"""Handles errors emitted from commands.""" + +import discord +from botcore.utils import logging +from discord.ext import commands + +log = logging.get_logger(__name__) + + +SUPPRESSED_ERRORS = ( +    commands.errors.CommandNotFound, +    commands.errors.CheckFailure, +) + + +class ErrorHandler(commands.Cog): +    """Handles errors emitted from commands.""" + +    def __init__(self, bot: commands.Bot) -> None: +        self.bot = bot + +    def _get_error_embed(self, title: str, body: str) -> discord.Embed: +        """Return an embed that contains the exception.""" +        return discord.Embed( +            title=title, +            colour=discord.Colour.red(), +            description=body +        ) + +    @commands.Cog.listener() +    async def on_command_error(self, ctx: commands.Context, e: commands.errors.CommandError) -> None: +        """Provide generic command error handling.""" +        if isinstance(e, SUPPRESSED_ERRORS): +            log.debug( +                f"Command {ctx.invoked_with} invoked by {ctx.message.author} with error " +                f"{e.__class__.__name__}: {e}" +            ) + + +async def setup(bot: commands.Bot) -> None: +    """Load the ErrorHandler cog.""" +    await bot.add_cog(ErrorHandler(bot)) diff --git a/metricity/exts/event_listeners/__init__.py b/metricity/exts/event_listeners/__init__.py new file mode 100644 index 0000000..2830bfb --- /dev/null +++ b/metricity/exts/event_listeners/__init__.py @@ -0,0 +1 @@ +"""A module containing all extensions around listening to events and storing them in the database.""" diff --git a/metricity/exts/event_listeners/_utils.py b/metricity/exts/event_listeners/_utils.py new file mode 100644 index 0000000..f0bbe39 --- /dev/null +++ b/metricity/exts/event_listeners/_utils.py @@ -0,0 +1,36 @@ +import discord + +from metricity import models + + +async def insert_thread(thread: discord.Thread) -> None: +    """Insert the given thread to the database.""" +    await models.Thread.create( +        id=str(thread.id), +        parent_channel_id=str(thread.parent_id), +        name=thread.name, +        archived=thread.archived, +        auto_archive_duration=thread.auto_archive_duration, +        locked=thread.locked, +        type=thread.type.name, +    ) + + +async def sync_message(message: discord.Message, from_thread: bool) -> None: +    """Sync the given message with the database.""" +    if await models.Message.get(str(message.id)): +        return + +    args = { +        "id": str(message.id), +        "channel_id": str(message.channel.id), +        "author_id": str(message.author.id), +        "created_at": message.created_at +    } + +    if from_thread: +        thread = message.channel +        args["channel_id"] = str(thread.parent_id) +        args["thread_id"] = str(thread.id) + +    await models.Message.create(**args) diff --git a/metricity/exts/event_listeners/guild_listeners.py b/metricity/exts/event_listeners/guild_listeners.py new file mode 100644 index 0000000..18eb79a --- /dev/null +++ b/metricity/exts/event_listeners/guild_listeners.py @@ -0,0 +1,187 @@ +"""An ext to listen for guild (and guild channel) events and syncs them to the database.""" + +import discord +from botcore.utils import logging, scheduling +from discord.ext import commands + +from metricity import models +from metricity.bot import Bot +from metricity.config import BotConfig +from metricity.database import db +from metricity.exts.event_listeners import _utils + +log = logging.get_logger(__name__) + + +class GuildListeners(commands.Cog): +    """Listen for guild (and guild channel) events and sync them to the database.""" + +    def __init__(self, bot: Bot) -> None: +        self.bot = bot +        scheduling.create_task(self.sync_guild()) + +    async def sync_guild(self) -> None: +        """Sync all channels and members in the guild.""" +        await self.bot.wait_until_guild_available() + +        guild = self.bot.get_guild(self.bot.guild_id) +        await self.sync_channels(guild) + +        log.info("Beginning thread archive state synchronisation process") +        await self.sync_thread_archive_state(guild) + +        log.info("Beginning user synchronisation process") +        await models.User.update.values(in_guild=False).gino.status() + +        users = [] +        for user in guild.members: +            users.append({ +                "id": str(user.id), +                "name": user.name, +                "avatar_hash": getattr(user.avatar, "key", None), +                "guild_avatar_hash": getattr(user.guild_avatar, "key", None), +                "joined_at": user.joined_at, +                "created_at": user.created_at, +                "is_staff": BotConfig.staff_role_id in [role.id for role in user.roles], +                "bot": user.bot, +                "in_guild": True, +                "public_flags": dict(user.public_flags), +                "pending": user.pending +            }) + +        log.info(f"Performing bulk upsert of {len(users)} rows") + +        user_chunks = discord.utils.as_chunks(users, 500) + +        for chunk in user_chunks: +            log.info(f"Upserting chunk of {len(chunk)}") +            await models.User.bulk_upsert(chunk) + +        log.info("User upsert complete") + +        self.bot.sync_process_complete.set() + +    @staticmethod +    async def sync_thread_archive_state(guild: discord.Guild) -> None: +        """Sync the archive state of all threads in the database with the state in guild.""" +        active_thread_ids = [str(thread.id) for thread in guild.threads] +        async with db.transaction() as tx: +            async for db_thread in tx.connection.iterate(models.Thread.query): +                await db_thread.update(archived=db_thread.id not in active_thread_ids).apply() + +    async def sync_channels(self, guild: discord.Guild) -> None: +        """Sync channels and categories with the database.""" +        self.bot.channel_sync_in_progress.clear() + +        log.info("Beginning category synchronisation process") + +        for channel in guild.channels: +            if isinstance(channel, discord.CategoryChannel): +                if db_cat := await models.Category.get(str(channel.id)): +                    await db_cat.update(name=channel.name).apply() +                else: +                    await models.Category.create(id=str(channel.id), name=channel.name) + +        log.info("Category synchronisation process complete, synchronising channels") + +        for channel in guild.channels: +            if channel.category: +                if channel.category.id in BotConfig.ignore_categories: +                    continue + +            if not isinstance(channel, discord.CategoryChannel): +                category_id = str(channel.category.id) if channel.category else None +                # Cast to bool so is_staff is False if channel.category is None +                is_staff = bool( +                    channel.category +                    and channel.category.id in BotConfig.staff_categories +                ) +                if db_chan := await models.Channel.get(str(channel.id)): +                    await db_chan.update( +                        name=channel.name, +                        category_id=category_id, +                        is_staff=is_staff, +                    ).apply() +                else: +                    await models.Channel.create( +                        id=str(channel.id), +                        name=channel.name, +                        category_id=category_id, +                        is_staff=is_staff, +                    ) + +        log.info("Channel synchronisation process complete, synchronising threads") + +        for thread in guild.threads: +            if thread.parent and thread.parent.category: +                if thread.parent.category.id in BotConfig.ignore_categories: +                    continue +            else: +                # This is a forum channel, not currently supported by Discord.py. Ignore it. +                continue + +            if db_thread := await models.Thread.get(str(thread.id)): +                await db_thread.update( +                    name=thread.name, +                    archived=thread.archived, +                    auto_archive_duration=thread.auto_archive_duration, +                    locked=thread.locked, +                    type=thread.type.name, +                ).apply() +            else: +                await _utils.insert_thread(thread) + +        log.info("Thread synchronisation process complete, finished synchronising guild.") +        self.bot.channel_sync_in_progress.set() + +    @commands.Cog.listener() +    async def on_guild_channel_create(self, channel: discord.abc.GuildChannel) -> None: +        """Sync the channels when one is created.""" +        if channel.guild.id != BotConfig.guild_id: +            return + +        await self.sync_channels(channel.guild) + +    @commands.Cog.listener() +    async def on_guild_channel_update( +        self, +        _before: discord.abc.GuildChannel, +        channel: discord.abc.GuildChannel +    ) -> None: +        """Sync the channels when one is updated.""" +        if channel.guild.id != BotConfig.guild_id: +            return + +        await self.sync_channels(channel.guild) + +    @commands.Cog.listener() +    async def on_thread_create(self, thread: discord.Thread) -> None: +        """Sync channels when a thread is created.""" +        if thread.guild.id != BotConfig.guild_id: +            return + +        await self.sync_channels(thread.guild) + +    @commands.Cog.listener() +    async def on_thread_update(self, _before: discord.Thread, thread: discord.Thread) -> None: +        """Sync the channels when one is updated.""" +        if thread.guild.id != BotConfig.guild_id: +            return + +        await self.sync_channels(thread.guild) + +    @commands.Cog.listener() +    async def on_guild_available(self, guild: discord.Guild) -> None: +        """Synchronize the user table with the Discord users.""" +        log.info(f"Received guild available for {guild.id}") + +        if guild.id != BotConfig.guild_id: +            log.info("Guild was not the configured guild, discarding event") +            return + +        await self.sync_guild() + + +async def setup(bot: Bot) -> None: +    """Load the GuildListeners cog.""" +    await bot.add_cog(GuildListeners(bot)) diff --git a/metricity/exts/event_listeners/member_listeners.py b/metricity/exts/event_listeners/member_listeners.py new file mode 100644 index 0000000..f3074ce --- /dev/null +++ b/metricity/exts/event_listeners/member_listeners.py @@ -0,0 +1,124 @@ +"""An ext to listen for member events and syncs them to the database.""" + +import discord +from asyncpg.exceptions import UniqueViolationError +from discord.ext import commands + +from metricity.bot import Bot +from metricity.config import BotConfig +from metricity.models import User + + +class MemberListeners(commands.Cog): +    """Listen for member events and sync them to the database.""" + +    def __init__(self, bot: Bot) -> None: +        self.bot = bot + +    @commands.Cog.listener() +    async def on_member_remove(self, member: discord.Member) -> None: +        """On a user leaving the server mark in_guild as False.""" +        await self.bot.sync_process_complete.wait() + +        if member.guild.id != BotConfig.guild_id: +            return + +        if db_user := await User.get(str(member.id)): +            await db_user.update( +                in_guild=False +            ).apply() + +    @commands.Cog.listener() +    async def on_member_join(self, member: discord.Member) -> None: +        """On a user joining the server add them to the database.""" +        await self.bot.sync_process_complete.wait() + +        if member.guild.id != BotConfig.guild_id: +            return + +        if db_user := await User.get(str(member.id)): +            await db_user.update( +                id=str(member.id), +                name=member.name, +                avatar_hash=getattr(member.avatar, "key", None), +                guild_avatar_hash=getattr(member.guild_avatar, "key", None), +                joined_at=member.joined_at, +                created_at=member.created_at, +                is_staff=BotConfig.staff_role_id in [role.id for role in member.roles], +                public_flags=dict(member.public_flags), +                pending=member.pending, +                in_guild=True +            ).apply() +        else: +            try: +                await User.create( +                    id=str(member.id), +                    name=member.name, +                    avatar_hash=getattr(member.avatar, "key", None), +                    guild_avatar_hash=getattr(member.guild_avatar, "key", None), +                    joined_at=member.joined_at, +                    created_at=member.created_at, +                    is_staff=BotConfig.staff_role_id in [role.id for role in member.roles], +                    public_flags=dict(member.public_flags), +                    pending=member.pending, +                    in_guild=True +                ) +            except UniqueViolationError: +                pass + +    @commands.Cog.listener() +    async def on_member_update(self, before: discord.Member, member: discord.Member) -> None: +        """When a member updates their profile, update the DB record.""" +        await self.bot.sync_process_complete.wait() + +        if member.guild.id != BotConfig.guild_id: +            return + +        # Joined at will be null if we are not ready to process events yet +        if not member.joined_at: +            return + +        roles = set([role.id for role in member.roles]) + +        if db_user := await User.get(str(member.id)): +            if ( +                db_user.name != member.name or +                db_user.avatar_hash != getattr(member.avatar, "key", None) or +                db_user.guild_avatar_hash != getattr(member.guild_avatar, "key", None) or +                BotConfig.staff_role_id in +                [role.id for role in member.roles] != db_user.is_staff +                or db_user.pending is not member.pending +            ): +                await db_user.update( +                    id=str(member.id), +                    name=member.name, +                    avatar_hash=getattr(member.avatar, "key", None), +                    guild_avatar_hash=getattr(member.guild_avatar, "key", None), +                    joined_at=member.joined_at, +                    created_at=member.created_at, +                    is_staff=BotConfig.staff_role_id in roles, +                    public_flags=dict(member.public_flags), +                    in_guild=True, +                    pending=member.pending +                ).apply() +        else: +            try: +                await User.create( +                    id=str(member.id), +                    name=member.name, +                    avatar_hash=getattr(member.avatar, "key", None), +                    guild_avatar_hash=getattr(member.guild_avatar, "key", None), +                    joined_at=member.joined_at, +                    created_at=member.created_at, +                    is_staff=BotConfig.staff_role_id in roles, +                    public_flags=dict(member.public_flags), +                    in_guild=True, +                    pending=member.pending +                ) +            except UniqueViolationError: +                pass + + +async def setup(bot: Bot) -> None: +    """Load the MemberListeners cog.""" +    await bot.add_cog(MemberListeners(bot)) diff --git a/metricity/exts/event_listeners/message_listeners.py b/metricity/exts/event_listeners/message_listeners.py new file mode 100644 index 0000000..b446e26 --- /dev/null +++ b/metricity/exts/event_listeners/message_listeners.py @@ -0,0 +1,62 @@ +"""An ext to listen for message events and syncs them to the database.""" + +import discord +from discord.ext import commands + +from metricity.bot import Bot +from metricity.config import BotConfig +from metricity.exts.event_listeners import _utils +from metricity.models import Message, User + + +class MessageListeners(commands.Cog): +    """Listen for message events and sync them to the database.""" + +    def __init__(self, bot: Bot) -> None: +        self.bot = bot + +    @commands.Cog.listener() +    async def on_message(self, message: discord.Message) -> None: +        """Add a message to the table when one is sent providing the author has accepted.""" +        if not message.guild: +            return + +        if message.author.bot: +            return + +        if message.guild.id != BotConfig.guild_id: +            return + +        if message.type in (discord.MessageType.thread_created, discord.MessageType.auto_moderation_action): +            return + +        await self.bot.sync_process_complete.wait() +        await self.bot.channel_sync_in_progress.wait() + +        if not await User.get(str(message.author.id)): +            return + +        cat_id = message.channel.category.id if message.channel.category else None +        if cat_id in BotConfig.ignore_categories: +            return + +        from_thread = isinstance(message.channel, discord.Thread) +        await _utils.sync_message(message, from_thread=from_thread) + +    @commands.Cog.listener() +    async def on_raw_message_delete(self, message: discord.RawMessageDeleteEvent) -> None: +        """If a message is deleted and we have a record of it set the is_deleted flag.""" +        if message := await Message.get(str(message.message_id)): +            await message.update(is_deleted=True).apply() + +    @commands.Cog.listener() +    async def on_raw_bulk_message_delete(self, messages: discord.RawBulkMessageDeleteEvent) -> None: +        """If messages are deleted in bulk and we have a record of them set the is_deleted flag.""" +        for message_id in messages.message_ids: +            if message := await Message.get(str(message_id)): +                await message.update(is_deleted=True).apply() + + +async def setup(bot: Bot) -> None: +    """Load the MessageListeners cog.""" +    await bot.add_cog(MessageListeners(bot)) diff --git a/metricity/exts/status.py b/metricity/exts/status.py new file mode 100644 index 0000000..c69e0c1 --- /dev/null +++ b/metricity/exts/status.py @@ -0,0 +1,60 @@ +"""Basic status commands to check the health of the bot.""" +import datetime + +import discord +from discord.ext import commands + +from metricity.config import BotConfig + +DESCRIPTIONS = ( +    "Command processing time", +    "Last event received", +    "Discord API latency" +) +ROUND_LATENCY = 3 +INTRO_MESSAGE = "Hello, I'm {name}. I insert all your data into a GDPR-compliant database." + + +class Status(commands.Cog): +    """Get the latency between the bot and Discord.""" + +    def __init__(self, bot: commands.Bot) -> None: +        self.bot = bot + +    @commands.Cog.listener() +    async def on_socket_event_type(self, _: str) -> None: +        """Store the last event received as an int.""" +        self.last_event_received = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) + +    @commands.command() +    @commands.has_any_role(BotConfig.staff_role_id) +    @commands.guild_only() +    async def status(self, ctx: commands.Context) -> None: +        """Respond with an embed with useful status info for debugging.""" +        if ctx.guild.id != BotConfig.guild_id: +            return + +        bot_ping = (datetime.datetime.now(datetime.timezone.utc) - ctx.message.created_at).total_seconds() * 1000 +        if bot_ping <= 0: +            bot_ping = "Your clock is out of sync, could not calculate ping." +        else: +            bot_ping = f"{bot_ping:.{ROUND_LATENCY}f} ms" + +        discord_ping = f"{self.bot.latency * 1000:.{ROUND_LATENCY}f} ms" + +        last_event = f"<t:{self.last_event_received}>" + +        embed = discord.Embed( +            title="Status", +            description=INTRO_MESSAGE.format(name=ctx.guild.me.display_name), +        ) + +        for desc, latency in zip(DESCRIPTIONS, (bot_ping, last_event, discord_ping)): +            embed.add_field(name=desc, value=latency, inline=False) + +        await ctx.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: +    """Load the status extension.""" +    await bot.add_cog(Status(bot)) diff --git a/metricity/models.py b/metricity/models.py index 87be19b..4f136de 100644 --- a/metricity/models.py +++ b/metricity/models.py @@ -5,8 +5,7 @@ from typing import Any, Dict, List  from sqlalchemy.dialects.postgresql import insert -from metricity.database import db -from metricity.utils import TZDateTime +from metricity.database import TZDateTime, db  class Category(db.Model): diff --git a/metricity/utils.py b/metricity/utils.py deleted file mode 100644 index 3f3547c..0000000 --- a/metricity/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -"""General utility functions and classes for Metricity.""" -from datetime import datetime, timezone - -from sqlalchemy.engine import Dialect -from sqlalchemy.types import DateTime, TypeDecorator - - -class TZDateTime(TypeDecorator): -    """ -    A db type that supports the use of aware datetimes in user-land. - -    Source from SQLAlchemy docs: -    https://docs.sqlalchemy.org/en/14/core/custom_types.html#store-timezone-aware-timestamps-as-timezone-naive-utc - -    Editted to include docstrings and type hints. -    """ - -    impl = DateTime -    cache_ok = True - -    def process_bind_param(self, value: datetime, dialect: Dialect) -> datetime: -        """Convert the value to aware before saving to db.""" -        if value is not None: -            if not value.tzinfo: -                raise TypeError("tzinfo is required") -            value = value.astimezone(timezone.utc).replace( -                tzinfo=None -            ) -        return value - -    def process_result_value(self, value: datetime, dialect: Dialect) -> datetime: -        """Convert the value to aware before passing back to user-land.""" -        if value is not None: -            value = value.replace(tzinfo=timezone.utc) -        return value diff --git a/poetry.lock b/poetry.lock index 511044f..a1c005c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,33 +1,54 @@  [[package]] +name = "aiodns" +version = "3.0.0" +description = "Simple DNS resolver for asyncio" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycares = ">=4.0.0" + +[[package]]  name = "aiohttp" -version = "3.7.4.post0" +version = "3.8.1"  description = "Async http client/server framework (asyncio)"  category = "main"  optional = false  python-versions = ">=3.6"  [package.dependencies] -async-timeout = ">=3.0,<4.0" +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0"  attrs = ">=17.3.0" -chardet = ">=2.0,<5.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1"  multidict = ">=4.5,<7.0" -typing-extensions = ">=3.6.5"  yarl = ">=1.0,<2.0"  [package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] +speedups = ["aiodns", "brotli", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.2.0" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +frozenlist = ">=1.1.0"  [[package]]  name = "alembic" -version = "1.7.7" +version = "1.8.0"  description = "A database migration tool for SQLAlchemy."  category = "main"  optional = false -python-versions = ">=3.6" +python-versions = ">=3.7"  [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} -importlib-resources = {version = "*", markers = "python_version < \"3.9\""}  Mako = "*"  SQLAlchemy = ">=1.3.0" @@ -36,15 +57,15 @@ tz = ["python-dateutil"]  [[package]]  name = "async-timeout" -version = "3.0.1" +version = "4.0.2"  description = "Timeout context manager for asyncio programs"  category = "main"  optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.6"  [[package]]  name = "asyncpg" -version = "0.25.0" +version = "0.26.0"  description = "An asyncio PostgreSQL driver"  category = "main"  optional = false @@ -70,30 +91,62 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)"  tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]  [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "bot-core" +version = "7.2.2" +description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community."  category = "main"  optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "3.9.*" + +[package.dependencies] +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} +statsd = "3.3.0" + +[package.extras] +async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"] + +[package.source] +type = "url" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.2.2.zip" +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"]  [[package]]  name = "coloredlogs" -version = "14.3" +version = "15.0.1"  description = "Colored terminal output for Python's logging module"  category = "main"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"  [package.dependencies] -humanfriendly = ">=7.1" +humanfriendly = ">=9.1"  [package.extras]  cron = ["capturer (>=2.4)"]  [[package]]  name = "deepmerge" -version = "0.1.1" +version = "1.0.1"  description = "a toolset to deeply merge python dictionaries."  category = "main"  optional = false @@ -108,32 +161,34 @@ optional = false  python-versions = ">=3.8.0"  [package.dependencies] -aiohttp = ">=3.6.0,<3.8.0" +aiohttp = ">=3.7.4,<4"  [package.extras] -docs = ["sphinx (==4.0.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] -speed = ["orjson (>=3.5.4)"] -voice = ["PyNaCl (>=1.3.0,<1.5)"] +docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions"] +speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet"] +test = ["coverage", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock"] +voice = ["PyNaCl (>=1.3.0,<1.6)"]  [package.source]  type = "url" -url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" +url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip" +  [[package]]  name = "flake8" -version = "3.9.2" +version = "4.0.1"  description = "the modular source code checker: pep8 pyflakes and co"  category = "dev"  optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6"  [package.dependencies]  mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0"  [[package]]  name = "flake8-annotations" -version = "2.8.0" +version = "2.9.0"  description = "Flake8 Type Annotation Checks"  category = "dev"  optional = false @@ -167,6 +222,14 @@ python-versions = "*"  pycodestyle = "*"  [[package]] +name = "frozenlist" +version = "1.3.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]]  name = "gino"  version = "1.0.1"  description = "GINO Is Not ORM - a Python asyncio ORM on SQLAlchemy core." @@ -205,39 +268,8 @@ optional = false  python-versions = ">=3.5"  [[package]] -name = "importlib-metadata" -version = "4.11.3" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] - -[[package]] -name = "importlib-resources" -version = "5.6.0" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] - -[[package]]  name = "mako" -version = "1.2.0" +version = "1.2.1"  description = "A super-fast templating language that borrows the best ideas from the existing templating languages."  category = "main"  optional = false @@ -284,11 +316,33 @@ optional = false  python-versions = ">=3.6"  [[package]] +name = "pycares" +version = "4.2.1" +description = "Python interface for c-ares" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.5.0" + +[package.extras] +idna = ["idna (>=2.1)"] + +[[package]]  name = "pycodestyle" -version = "2.7.0" +version = "2.8.0"  description = "Python style guide checker"  category = "dev"  optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"  [[package]] @@ -307,7 +361,7 @@ toml = ["toml"]  [[package]]  name = "pyflakes" -version = "2.3.1" +version = "2.4.0"  description = "passive checker of Python programs"  category = "dev"  optional = false @@ -323,11 +377,11 @@ python-versions = "*"  [[package]]  name = "python-dotenv" -version = "0.14.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.20.0" +description = "Read key-value pairs from a .env file and set them as environment variables"  category = "main"  optional = false -python-versions = "*" +python-versions = ">=3.5"  [package.extras]  cli = ["click (>=5.0)"] @@ -361,20 +415,20 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]  pymysql = ["pymysql (<1)", "pymysql"]  [[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" +name = "statsd" +version = "3.3.0" +description = "A simple statsd client."  category = "main"  optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "*"  [[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language"  category = "main"  optional = false -python-versions = ">=3.6" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"  [[package]]  name = "yarl" @@ -388,123 +442,188 @@ python-versions = ">=3.6"  idna = ">=2.0"  multidict = ">=4.0" -[[package]] -name = "zipp" -version = "3.8.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] -  [metadata]  lock-version = "1.1" -python-versions = "~3.8 || ~3.9" -content-hash = "6c5c044b09fb33ae21e78e1154725318bf7f9d4d3f12ab5b484e46bf960aa9c2" +python-versions = "~3.9" +content-hash = "f56b08a8313ceea84edcbb81fbcf8a71d299a26b977ed667e72f47d359d46c80"  [metadata.files] +aiodns = [ +    {file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"}, +    {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, +]  aiohttp = [ -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, -    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, -    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, -    {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, -    {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, -    {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, -] -alembic = [ -    {file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"}, -    {file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"}, +    {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, +    {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, +    {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, +    {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, +    {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, +    {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, +    {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, +    {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, +    {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, +    {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, +    {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, +    {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, +    {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, +    {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, +    {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, +    {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, +    {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, +    {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, +    {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, +    {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, +    {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, +    {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, +    {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, +    {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, +    {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, +    {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, +    {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, +    {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, +    {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, +    {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, +    {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, +    {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, +    {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, +    {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, +    {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, +    {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, +    {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, +    {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, +    {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, +    {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, +    {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, +    {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, +    {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, +    {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, +    {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, +    {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, +    {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, +    {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},  ] +aiosignal = [ +    {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, +    {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] +alembic = []  async-timeout = [ -    {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, -    {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, -] -asyncpg = [ -    {file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"}, -    {file = "asyncpg-0.25.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a"}, -    {file = "asyncpg-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4"}, -    {file = "asyncpg-0.25.0-cp310-cp310-win32.whl", hash = "sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095"}, -    {file = "asyncpg-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09"}, -    {file = "asyncpg-0.25.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634"}, -    {file = "asyncpg-0.25.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5"}, -    {file = "asyncpg-0.25.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92"}, -    {file = "asyncpg-0.25.0-cp36-cp36m-win32.whl", hash = "sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4"}, -    {file = "asyncpg-0.25.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd"}, -    {file = "asyncpg-0.25.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68"}, -    {file = "asyncpg-0.25.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b"}, -    {file = "asyncpg-0.25.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256"}, -    {file = "asyncpg-0.25.0-cp37-cp37m-win32.whl", hash = "sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9"}, -    {file = "asyncpg-0.25.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8"}, -    {file = "asyncpg-0.25.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b"}, -    {file = "asyncpg-0.25.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e"}, -    {file = "asyncpg-0.25.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962"}, -    {file = "asyncpg-0.25.0-cp38-cp38-win32.whl", hash = "sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471"}, -    {file = "asyncpg-0.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6"}, -    {file = "asyncpg-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e"}, -    {file = "asyncpg-0.25.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855"}, -    {file = "asyncpg-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e"}, -    {file = "asyncpg-0.25.0-cp39-cp39-win32.whl", hash = "sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2"}, -    {file = "asyncpg-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac"}, -    {file = "asyncpg-0.25.0.tar.gz", hash = "sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540"}, +    {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, +    {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},  ] +asyncpg = []  attrs = [      {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},      {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},  ] -chardet = [ -    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, -    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +bot-core = [] +cffi = [ +    {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, +    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, +    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, +    {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, +    {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, +    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, +    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, +    {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, +    {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, +    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, +    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, +    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, +    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, +    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, +    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, +    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, +    {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, +    {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, +    {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, +    {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, +    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, +    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, +    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, +    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, +    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, +    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, +    {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, +    {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, +    {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, +    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, +    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, +    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, +    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, +    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, +    {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, +    {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, +    {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, +    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, +    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, +    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, +    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, +    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, +    {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, +    {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, +    {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, +    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, +    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, +    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, +    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, +    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, +    {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, +    {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, +    {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, +    {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, +    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, +    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, +    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, +    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, +    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, +    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, +    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, +    {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, +    {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, +    {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},  ] -coloredlogs = [ -    {file = "coloredlogs-14.3-py2.py3-none-any.whl", hash = "sha256:e244a892f9d97ffd2c60f15bf1d2582ef7f9ac0f848d132249004184785702b3"}, -    {file = "coloredlogs-14.3.tar.gz", hash = "sha256:7ef1a7219870c7f02c218a2f2877ce68f2f8e087bb3a55bd6fbaa2a4362b4d52"}, +charset-normalizer = [ +    {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, +    {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},  ] -deepmerge = [ -    {file = "deepmerge-0.1.1-py2.py3-none-any.whl", hash = "sha256:190e133a6657303db37f9bb302aa853d8d2b15a0e055d41b99a362598e79206a"}, -    {file = "deepmerge-0.1.1.tar.gz", hash = "sha256:fa1d44269786bcc12d30a7471b0b39478aa37a43703b134d7f12649792f92c1f"}, +coloredlogs = [ +    {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, +    {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},  ] +deepmerge = []  "discord.py" = []  flake8 = [ -    {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, -    {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +    {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, +    {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},  ]  flake8-annotations = [ -    {file = "flake8-annotations-2.8.0.tar.gz", hash = "sha256:a2765c6043098aab0a3f519b871b33586c7fba7037686404b920cf8100cc1cdc"}, -    {file = "flake8_annotations-2.8.0-py3-none-any.whl", hash = "sha256:880f9bb0677b82655f9021112d64513e03caefd2e0d786ab4a59ddb5b262caa9"}, +    {file = "flake8-annotations-2.9.0.tar.gz", hash = "sha256:63fb3f538970b6a8dfd84125cf5af16f7b22e52d5032acb3b7eb23645ecbda9b"}, +    {file = "flake8_annotations-2.9.0-py3-none-any.whl", hash = "sha256:84f46de2964cb18fccea968d9eafce7cf857e34d913d515120795b9af6498d56"},  ]  flake8-docstrings = [      {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, @@ -514,6 +633,67 @@ flake8-import-order = [      {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"},      {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"},  ] +frozenlist = [ +    {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, +    {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, +    {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, +    {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, +    {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, +    {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, +    {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, +    {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, +    {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, +    {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, +    {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, +    {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, +    {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, +    {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, +    {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, +    {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, +    {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, +    {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, +    {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, +    {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, +    {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, +    {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, +    {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, +    {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, +    {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, +    {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, +    {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, +    {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, +    {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, +    {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, +    {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, +    {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, +    {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, +    {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, +    {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, +    {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, +    {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, +    {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, +    {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, +    {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, +    {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, +    {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, +    {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, +    {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, +    {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, +    {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, +    {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, +]  gino = [      {file = "gino-1.0.1-py3-none-any.whl", hash = "sha256:56df57cfdefbaf897a7c4897c265a0e91a8cca80716fb64f7d3cf6d501fdfb3d"},      {file = "gino-1.0.1.tar.gz", hash = "sha256:fe4189e82fe9d20c4a5f03fc775fb91c168061c5176b4c95623caeef22316150"}, @@ -526,18 +706,7 @@ idna = [      {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},      {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},  ] -importlib-metadata = [ -    {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, -    {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, -] -importlib-resources = [ -    {file = "importlib_resources-5.6.0-py3-none-any.whl", hash = "sha256:a9dd72f6cc106aeb50f6e66b86b69b454766dd6e39b69ac68450253058706bcc"}, -    {file = "importlib_resources-5.6.0.tar.gz", hash = "sha256:1b93238cbf23b4cde34240dd8321d99e9bf2eb4bc91c0c99b2886283e7baad85"}, -] -mako = [ -    {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, -    {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, -] +mako = []  markupsafe = [      {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},      {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, @@ -703,25 +872,62 @@ psycopg2-binary = [      {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},      {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},  ] +pycares = [ +    {file = "pycares-4.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d83f193563b42360528167705b1c7bb91e2a09f990b98e3d6378835b72cd5c96"}, +    {file = "pycares-4.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b03f69df69f0ab3bfb8dbe54444afddff6ff9389561a08aade96b4f91207a655"}, +    {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3b78bdee2f2f1351d5fccc2d1b667aea2d15a55d74d52cb9fd5bea8b5e74c4dc"}, +    {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f05223de13467bb26f9a1594a1799ce2d08ad8ea241489fecd9d8ed3bbbfc672"}, +    {file = "pycares-4.2.1-cp310-cp310-win32.whl", hash = "sha256:1f37f762414680063b4dfec5be809a84f74cd8e203d939aaf3ba9c807a9e7013"}, +    {file = "pycares-4.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:1a9506d496efeb809a1b63647cb2f3f33c67fcf62bf80a2359af692fef2c1755"}, +    {file = "pycares-4.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2fd53eb5b441c4f6f9c78d7900e05883e9998b34a14b804be4fc4c6f9fea89f3"}, +    {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:061dd4c80fec73feb150455b159704cd51a122f20d36790033bd6375d4198579"}, +    {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a521d7f54f3e52ded4d34c306ba05cfe9eb5aaa2e5aaf83c96564b9369495588"}, +    {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:99e00e397d07a79c9f43e4303e67f4f97bcabd013bda0d8f2d430509b7aef8a0"}, +    {file = "pycares-4.2.1-cp36-cp36m-win32.whl", hash = "sha256:d9cd826d8e0c270059450709bff994bfeb072f79d82fd3f11c701690ff65d0e7"}, +    {file = "pycares-4.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f8e6942965465ca98e212376c4afb9aec501d8129054929744b2f4a487c8c14b"}, +    {file = "pycares-4.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e75cbd4d3b3d9b02bba6e170846e39893a825e7a5fb1b96728fc6d7b964f8945"}, +    {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e8ec4c8e07c986b70a3cc8f5b297c53b08ac755e5b9797512002a466e2de86"}, +    {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5333b51ef4ff3e8973b4a1b57cad5ada13e15552445ee3cd74bd77407dec9d44"}, +    {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2113529004df4894783eaa61e9abc3a680756b6f033d942f2800301ae8c71c29"}, +    {file = "pycares-4.2.1-cp37-cp37m-win32.whl", hash = "sha256:e7a95763cdc20cf9ec357066e656ea30b8de6b03de6175cbb50890e22aa01868"}, +    {file = "pycares-4.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a901776163a04de5d67c42bd63a287cff9cb05fc041668ad1681fe3daa36445"}, +    {file = "pycares-4.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:66b5390a4885a578e687d3f2683689c35e1d4573f4d0ecf217431f7bb55c49a0"}, +    {file = "pycares-4.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15dd5cf21bc73ad539e8aabf7afe370d1df8af7bc6944cd7298f3bfef0c1a27c"}, +    {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ee625d7571039038bca51ae049b047cbfcfc024b302aae6cc53d5d9aa8648a8"}, +    {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:396ee487178e9de06ca4122a35a157474db3ce0a0db6038a31c831ebb9863315"}, +    {file = "pycares-4.2.1-cp38-cp38-win32.whl", hash = "sha256:e4dc37f732f7110ca6368e0128cbbd0a54f5211515a061b2add64da2ddb8e5ca"}, +    {file = "pycares-4.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:3636fccf643c5192c34ee0183c514a2d09419e3a76ca2717cef626638027cb21"}, +    {file = "pycares-4.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6724573e830ea2345f4bcf0f968af64cc6d491dc2133e9c617f603445dcdfa58"}, +    {file = "pycares-4.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dbfcacbde6c21380c412c13d53ea44b257dea3f7b9d80be2c873bb20e21fee"}, +    {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8a46839da642b281ac5f56d3c6336528e128b3c41eab9c5330d250f22325e9d"}, +    {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9b05c2cec644a6c66b55bcf6c24d4dfdaf2f7205b16e5c4ceee31db104fac958"}, +    {file = "pycares-4.2.1-cp39-cp39-win32.whl", hash = "sha256:8bd6ed3ad3a5358a635c1acf5d0f46be9afb095772b84427ff22283d2f31db1b"}, +    {file = "pycares-4.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:fbd53728d798d07811898e11991e22209229c090eab265a53d12270b95d70d1a"}, +    {file = "pycares-4.2.1.tar.gz", hash = "sha256:735b4f75fd0f595c4e9184da18cd87737f46bc81a64ea41f4edce2b6b68d46d2"}, +]  pycodestyle = [ -    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, -    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +    {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, +    {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pycparser = [ +    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, +    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},  ]  pydocstyle = [      {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},      {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},  ]  pyflakes = [ -    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, -    {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, +    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},  ]  pyreadline3 = [      {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},      {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},  ]  python-dotenv = [ -    {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"}, -    {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"}, +    {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, +    {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"},  ]  snowballstemmer = [      {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, @@ -763,14 +969,14 @@ sqlalchemy = [      {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"},      {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"},  ] +statsd = [ +    {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, +    {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, +]  toml = [      {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},      {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},  ] -typing-extensions = [ -    {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, -    {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -]  yarl = [      {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},      {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, @@ -845,7 +1051,3 @@ yarl = [      {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},      {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},  ] -zipp = [ -    {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, -    {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] diff --git a/pyproject.toml b/pyproject.toml index 1f50b07..8f9903c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,32 @@  [tool.poetry]  name = "metricity" -version = "1.3.1" +version = "1.4.0"  description = "Advanced metric collection for the Python Discord server"  authors = ["Joe Banks <[email protected]>"]  license = "MIT"  [tool.poetry.dependencies] -python = "~3.8 || ~3.9" -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"} -deepmerge = "^0.1.0" -toml = "^0.10.1" -coloredlogs = "^14.0" -python-dotenv = "^0.14.0" -gino = "^1.0.1" -alembic = "^1.4.2" -psycopg2-binary = "^2.8.5" +python = "~3.9" + +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} +# See https://bot-core.pythondiscord.com/ for docs. +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.2.2.zip"} + +aiodns = "3.0.0" +aiohttp = "3.8.1" +deepmerge = "1.0.1" +toml = "0.10.2" +coloredlogs = "15.0.1" +python-dotenv = "0.20.0" +gino = "1.0.1" +alembic = "1.8.0" +psycopg2-binary = "2.9.3"  [tool.poetry.dev-dependencies] -flake8 = "^3.8.3" -flake8-annotations = "^2.3.0" -flake8-docstrings = "^1.5.0" -flake8-import-order = "^0.18.1" +flake8 = "4.0.1" +flake8-annotations = "2.9.0" +flake8-docstrings = "1.6.0" +flake8-import-order = "0.18.1"  [tool.poetry.scripts]  start = "metricity.__main__:start" @@ -2,9 +2,13 @@  max-line-length=120  application-import-names=metricity  import-order-style=pycharm -exclude=alembic +exclude=alembic,.venv,.cache  extend-ignore=      # self params in classes.      ANN101, +    # args and kwargs +    ANN002, ANN003,      # line break before/after binary operator -    W503, W504 +    W503, W504, +    # __init__ doc strings +    D107 | 
