diff --git a/docs/contributing.md b/docs/contributing.md index 3c0bad0b..00d59bb0 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -149,6 +149,37 @@ $ poetry install This runs our register pre-commit hooks on every commit to automatically point out issues in code such as missing semicolons, trailing whitespace, and debug statements. By pointing these issues out before code review, this allows a code reviewer to focus on the architecture of a change while not wasting time with trivial style nitpicks. +### PostgreSQL setup + +Install PostgreSQL according to its [documentation](https://www.postgresql.org/download/). + +Enter psql, a terminal-based front-end to PostgreSQL: + +
+ +```console +$ psql -qd postgres +``` + +Run the following queries to create the user and database: + +```psql +CREATE USER voting WITH SUPERUSER PASSWORD 'modmail'; +CREATE DATABASE modmail WITH OWNER modmail; +``` + +Finally, enter `/q` to exit psql. + +Once the Database is started, you need run migrations to init tables and columns which can be ran through: + +
+ +```console +$ poetry run alembic upgrade heads + +---> 100% +``` + ### Set up modmail config @@ -182,7 +213,12 @@ $ poetry install !!!note The entire file name is literally `.env` -5. Open the file with any text editor and write the bot token to the files in this format: `TOKEN="my_token"`. +5. Open the file with any text editor and write the bot token and the database URL to the files in this format: + * `TOKEN="my_token"`. + * `SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://modmail:modmail@localhost:5432/modmail` + +!!! note + If you configured PostgreSQL in a different manner or you are not hosting it locally, then you will need to determine the correct host and port yourself. The user, password, and database name should all still be pysite unless you deviated from the setup instructions in the previous section. ### Run The Project diff --git a/modmail/__init__.py b/modmail/__init__.py index 9377ffd2..20a8c6d6 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -58,3 +58,4 @@ logging.getLogger("websockets").setLevel(logging.ERROR) # Set asyncio logging back to the default of INFO even if asyncio's debug mode is enabled. logging.getLogger("asyncio").setLevel(logging.INFO) +logging.getLogger("sqlalchemy").setLevel(logging.INFO) diff --git a/modmail/__main__.py b/modmail/__main__.py index fd10af2a..7938d541 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -1,3 +1,4 @@ +import asyncio import logging from modmail.bot import ModmailBot @@ -21,6 +22,9 @@ def main() -> None: """Run the bot.""" patch_embed() bot = ModmailBot() + # Check if the database is alive before running the bot + loop = asyncio.get_event_loop() + loop.run_until_complete(bot.init_db()) bot.run(bot.config.bot.token) diff --git a/modmail/alembic.ini b/modmail/alembic.ini new file mode 100644 index 00000000..9d0aa65c --- /dev/null +++ b/modmail/alembic.ini @@ -0,0 +1,81 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +[post_write_hooks] +hooks = black +black.type = console_scripts +black.entrypoint = black +black.options = -l 110 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/modmail/alembic/README b/modmail/alembic/README new file mode 100644 index 00000000..a23d4fb5 --- /dev/null +++ b/modmail/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration with an async dbapi. diff --git a/modmail/alembic/env.py b/modmail/alembic/env.py new file mode 100644 index 00000000..dbfd0b87 --- /dev/null +++ b/modmail/alembic/env.py @@ -0,0 +1,103 @@ +import asyncio +import inspect +import os +import sys +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine + + +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_parent_dir = os.path.dirname(os.path.dirname(current_dir)) +sys.path.insert(0, parent_parent_dir) + +from modmail.config import CONFIG # noqa: I201, E402 +from modmail.models.base import Base # noqa: I201, E402 + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_url() -> str: + """Get database connection url.""" + return CONFIG.bot.sqlalchemy_database_uri + + +def run_migrations_offline() -> None: + """ + Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + """ + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: AsyncConnection) -> None: + """Run migrations.""" + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online() -> None: + """ + Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = AsyncEngine( + engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + future=True, + ) + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/modmail/alembic/script.py.mako b/modmail/alembic/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/modmail/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/modmail/alembic/versions/500f4263ffe9_add_tickets_table.py b/modmail/alembic/versions/500f4263ffe9_add_tickets_table.py new file mode 100644 index 00000000..bc65ce0b --- /dev/null +++ b/modmail/alembic/versions/500f4263ffe9_add_tickets_table.py @@ -0,0 +1,33 @@ +"""Add tickets table + +Revision ID: 500f4263ffe9 +Revises: e6b20ed26bc2 +Create Date: 2021-08-28 17:01:39.611060 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "500f4263ffe9" +down_revision = "e6b20ed26bc2" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "tickets", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column("server_id", sa.BigInteger(), nullable=False), + sa.Column("thread_id", sa.BigInteger(), nullable=False), + sa.Column("creater_id", sa.BigInteger(), nullable=False), + sa.Column("creating_message_id", sa.BigInteger(), nullable=False), + sa.Column("creating_channel_id", sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade(): + op.drop_table("tickets") diff --git a/modmail/alembic/versions/72459556f7b9_add_configurations_table.py b/modmail/alembic/versions/72459556f7b9_add_configurations_table.py new file mode 100644 index 00000000..2272ce68 --- /dev/null +++ b/modmail/alembic/versions/72459556f7b9_add_configurations_table.py @@ -0,0 +1,30 @@ +"""Add configurations table + +Revision ID: 72459556f7b9 +Revises: 7b384834cca4 +Create Date: 2021-08-28 17:02:36.726948 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "72459556f7b9" +down_revision = "7b384834cca4" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "configurations", + sa.Column("target_id", sa.BigInteger(), nullable=False), + sa.Column("config_key", sa.String(), nullable=False), + sa.Column("config_value", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("target_id"), + ) + + +def downgrade(): + op.drop_table("configurations") diff --git a/modmail/alembic/versions/7b384834cca4_add_emojis_table.py b/modmail/alembic/versions/7b384834cca4_add_emojis_table.py new file mode 100644 index 00000000..1befb023 --- /dev/null +++ b/modmail/alembic/versions/7b384834cca4_add_emojis_table.py @@ -0,0 +1,31 @@ +"""Add emojis table + +Revision ID: 7b384834cca4 +Revises: 8d52443a026e +Create Date: 2021-08-28 17:02:30.226745 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "7b384834cca4" +down_revision = "8d52443a026e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "emojis", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("url", sa.String(), nullable=False), + sa.Column("animated", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade(): + op.drop_table("emojis") diff --git a/modmail/alembic/versions/8d52443a026e_add_attachments_table.py b/modmail/alembic/versions/8d52443a026e_add_attachments_table.py new file mode 100644 index 00000000..e4b646fa --- /dev/null +++ b/modmail/alembic/versions/8d52443a026e_add_attachments_table.py @@ -0,0 +1,34 @@ +"""Add attachments table + +Revision ID: 8d52443a026e +Revises: 9dacee669c96 +Create Date: 2021-08-28 17:02:15.788101 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "8d52443a026e" +down_revision = "9dacee669c96" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "attachments", + sa.Column("internal_id", sa.BigInteger(), nullable=False), + sa.Column("message_id", sa.BigInteger(), nullable=False), + sa.Column("link", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["message_id"], + ["messages.id"], + ), + sa.PrimaryKeyConstraint("internal_id"), + ) + + +def downgrade(): + op.drop_table("attachments") diff --git a/modmail/alembic/versions/9dacee669c96_add_embeds_table.py b/modmail/alembic/versions/9dacee669c96_add_embeds_table.py new file mode 100644 index 00000000..fd0ffc48 --- /dev/null +++ b/modmail/alembic/versions/9dacee669c96_add_embeds_table.py @@ -0,0 +1,34 @@ +"""Add embeds table + +Revision ID: 9dacee669c96 +Revises: 500f4263ffe9 +Create Date: 2021-08-28 17:02:06.180093 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "9dacee669c96" +down_revision = "500f4263ffe9" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "embeds", + sa.Column("internal_id", sa.BigInteger(), nullable=False), + sa.Column("message_id", sa.BigInteger(), nullable=False), + sa.Column("json_content", sa.JSON(), nullable=False), + sa.ForeignKeyConstraint( + ["message_id"], + ["messages.id"], + ), + sa.PrimaryKeyConstraint("internal_id"), + ) + + +def downgrade(): + op.drop_table("embeds") diff --git a/modmail/alembic/versions/e6b20ed26bc2_add_messages_table.py b/modmail/alembic/versions/e6b20ed26bc2_add_messages_table.py new file mode 100644 index 00000000..8ace6314 --- /dev/null +++ b/modmail/alembic/versions/e6b20ed26bc2_add_messages_table.py @@ -0,0 +1,32 @@ +"""Add messages table + +Revision ID: e6b20ed26bc2 +Revises: +Create Date: 2021-08-28 17:01:24.449928 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "e6b20ed26bc2" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "messages", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column("ticket_id", sa.BigInteger(), nullable=False), + sa.Column("mirrored_id", sa.BigInteger(), nullable=False), + sa.Column("author_id", sa.BigInteger(), nullable=False), + sa.Column("content", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade(): + op.drop_table("messages") diff --git a/modmail/bot.py b/modmail/bot.py index e62f1bda..4f2e53cb 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -10,6 +10,8 @@ from discord import Activity, AllowedMentions, Intents from discord.client import _cleanup_loop from discord.ext import commands +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine +from sqlalchemy.orm import Session from modmail.config import CONFIG from modmail.log import ModmailLogger @@ -40,6 +42,8 @@ def __init__(self, **kwargs): self.config = CONFIG self.start_time: t.Optional[arrow.Arrow] = None # arrow.utcnow() self.http_session: t.Optional[ClientSession] = None + self._db_engine: t.Optional[AsyncEngine] = None + self.db: t.Optional[Session] = None status = discord.Status.online activity = Activity(type=discord.ActivityType.listening, name="users dming me!") @@ -49,6 +53,7 @@ def __init__(self, **kwargs): # allow only user mentions by default. # ! NOTE: This may change in the future to allow roles as well allowed_mentions = AllowedMentions(everyone=False, users=True, roles=False, replied_user=True) + super().__init__( case_insensitive=True, description="Modmail bot by discord-modmail.", @@ -60,12 +65,26 @@ def __init__(self, **kwargs): **kwargs, ) + async def init_db(self) -> None: + """Initiate the bot DB session and check if the DB is alive.""" + self._db_engine = create_async_engine(CONFIG.bot.sqlalchemy_database_uri, echo=True) + self.db = AsyncSession(self._db_engine) + try: + # Try to check if DB is awake + await self.db.execute("SELECT 1") + self.logger.notice(f"DB Connection pool established at {self._db_engine.url}") + except Exception as e: + self.logger.error( + f"DB connection at {CONFIG.bot.sqlalchemy_database_uri} not successful, raised\n{e}" + ) + exit() + async def start(self, token: str, reconnect: bool = True) -> None: """ Start the bot. This function is called by the run method, and finishes the set up of the bot that needs an - asyncrhonous event loop running, before connecting the bot to discord. + asynchronous event loop running, before connecting the bot to discord. """ try: # create the aiohttp session @@ -145,23 +164,29 @@ async def close(self) -> None: try: self.unload_extension(plug) except Exception: - self.logger.error(f"Exception occured while unloading plugin {plug.name}", exc_info=True) + self.logger.error(f"Exception occurred while unloading plugin {plug.name}", exc_info=True) for ext in list(self.extensions): try: self.unload_extension(ext) except Exception: - self.logger.error(f"Exception occured while unloading {ext.name}", exc_info=True) + self.logger.error(f"Exception occurred while unloading {ext.name}", exc_info=True) for cog in list(self.cogs): try: self.remove_cog(cog) except Exception: - self.logger.error(f"Exception occured while removing cog {cog.name}", exc_info=True) + self.logger.error(f"Exception occurred while removing cog {cog.name}", exc_info=True) if self.http_session: await self.http_session.close() + if self._db_engine: + await self._db_engine.dispose() + + if self.db: + await self.db.close() + await super().close() def load_extensions(self) -> None: diff --git a/modmail/config-default.toml b/modmail/config-default.toml index 29f7cfc5..bc85fa80 100644 --- a/modmail/config-default.toml +++ b/modmail/config-default.toml @@ -4,7 +4,7 @@ prefix = "?" [colors] [dev] -log_level = 25 # "NOTICE" +log_level = 25 # "NOTICE" [dev.mode] production = true diff --git a/modmail/config.py b/modmail/config.py index dce1efeb..4a1c881c 100644 --- a/modmail/config.py +++ b/modmail/config.py @@ -1,19 +1,11 @@ -import asyncio -import datetime -import json import logging import os -import sys import typing from pathlib import Path from typing import Any, Dict, Optional, Tuple -import discord import toml -from discord.ext.commands import BadArgument -from pydantic import BaseModel from pydantic import BaseSettings as PydanticBaseSettings -from pydantic import Field, SecretStr from pydantic.color import Color as ColorBase from pydantic.env_settings import SettingsSourceCallable from pydantic.types import conint @@ -37,7 +29,7 @@ def determine_file_path( for file_path in paths: config_file = Path(file_path) if (config_file).exists(): - path = config_file + path = str(config_file) log.debug(f"Found {config_type} config at {file_path}") break return path or None @@ -47,7 +39,7 @@ def determine_file_path( USER_CONFIG_PATH = determine_file_path(CONFIG_PATHS, config_type="") -def toml_default_config_source(settings: PydanticBaseSettings) -> Dict[str, Any]: +def toml_default_config_source(_: PydanticBaseSettings) -> Dict[str, Any]: """ A simple settings source that loads variables from a toml file from within the module's source folder. @@ -58,7 +50,7 @@ def toml_default_config_source(settings: PydanticBaseSettings) -> Dict[str, Any] return dict(**toml.load(DEFAULT_CONFIG_PATH)) -def toml_user_config_source(settings: PydanticBaseSettings) -> Dict[str, Any]: +def toml_user_config_source(_: PydanticBaseSettings) -> Dict[str, Any]: """ A simple settings source that loads variables from a toml file from within the module's source folder. @@ -96,7 +88,8 @@ def customise_sources( class BotConfig(BaseSettings): prefix: str = "?" - token: str = None + token: str = "" + sqlalchemy_database_uri: Optional[str] = None class Config: # env_prefix = "bot." diff --git a/modmail/crud/__init__.py b/modmail/crud/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modmail/crud/configurations.py b/modmail/crud/configurations.py new file mode 100644 index 00000000..d6dc0a9d --- /dev/null +++ b/modmail/crud/configurations.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session + +from modmail.models import Configurations + + +class CRUDConfigurations: + """View providing CRUD operations on Configurations.""" + + def get_by_config_key(self, db: Session, *, target_id: int, config_key: str) -> Optional[Configurations]: + """Get `Configurations` object by config key for `target_id`.""" + return ( + db.query(Configurations) + .filter(Configurations.target_id == target_id) + .filter(Configurations.config_key == config_key) + .first() + ) + + def get_multi_by_target( + self, db: Session, *, target_id: int, skip: int = 0, limit: int = 100 + ) -> List[Configurations]: + """Get all `Configurations` for `target_id`.""" + return ( + db.query(Configurations) + .filter(Configurations.target_id == target_id) + .offset(skip) + .limit(limit) + .all() + ) + + def create(self, db: Session, **kwargs) -> Configurations: + """Create a new `Configurations` object.""" + db_obj = Configurations(**kwargs) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update(self, db: Session, *, db_obj: Configurations, obj_in: Dict[str, Any]) -> Configurations: + """Update a `Configurations` object.""" + for field in obj_in: + setattr(db_obj, field, obj_in[field]) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def delete(self, db: Session, *, target_id: int, config_key: str) -> Configurations: + """Remove a `Configurations` object from the database.""" + obj = self.get_by_config_key(db, target_id=target_id, config_key=config_key) + db.delete(obj) + db.commit() + return obj diff --git a/modmail/crud/messages.py b/modmail/crud/messages.py new file mode 100644 index 00000000..c0bf027c --- /dev/null +++ b/modmail/crud/messages.py @@ -0,0 +1,25 @@ +from typing import Any + +from sqlalchemy.orm import Session + +from modmail.models import Messages + + +class CRUDMessages: + """View providing CRUD operations on Messages.""" + + def get_by_id(self, db: Session, id: Any) -> Messages: + """Get `Messages` object by object ID.""" + return db.query(Messages).filter(Messages.id == id).first() + + def get_by_mirrored_id(self, db: Session, *, mirrored_id: Any) -> Messages: + """Get `Messages` object by mirrored_id, ID of message in the server.""" + return db.query(Messages).filter(Messages.mirrored_id == mirrored_id).first() + + def create(self, db: Session, **kwargs) -> Messages: + """Create a new `Messages` object.""" + db_obj = Messages(**kwargs) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj diff --git a/modmail/crud/threads.py b/modmail/crud/threads.py new file mode 100644 index 00000000..906744d9 --- /dev/null +++ b/modmail/crud/threads.py @@ -0,0 +1,49 @@ +from typing import Any, List + +from sqlalchemy.orm import Session + +from modmail.models import Tickets + + +class CRUDTickets: + """View providing CRUD operations on Tickets.""" + + def get_by_id(self, db: Session, id: Any) -> Tickets: + """Get `Tickets` object by object ID.""" + return db.query(Tickets).filter(Tickets.id == id).first() + + def get_multi_by_creator( + self, db: Session, *, creater_id: int, server_id: int, skip: int = 0, limit: int = 100 + ) -> List[Tickets]: + """Get all `Tickets` of a user from server with `server_id`.""" + return ( + db.query(Tickets) + .filter(Tickets.creater_id == creater_id) + .filter(Tickets.server_id == server_id) + .offset(skip) + .limit(limit) + .all() + ) + + def get_multi_by_server( + self, db: Session, *, server_id: int, skip: int = 0, limit: int = 100 + ) -> List[Tickets]: + """Get all `Tickets` for a specific server.""" + return db.query(Tickets).filter(Tickets.server_id == server_id).offset(skip).limit(limit).all() + + def get_by_thread_id(self, db: Session, *, server_id: int, thread_id: int) -> Tickets: + """Get `Tickets` object by thread ID.""" + return ( + db.query(Tickets) + .filter(Tickets.server_id == server_id) + .filter(Tickets.thread_id == thread_id) + .first() + ) + + def create(self, db: Session, **kwargs) -> Tickets: + """Create a new `Tickets` object.""" + db_obj = Tickets(**kwargs) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj diff --git a/modmail/models/__init__.py b/modmail/models/__init__.py new file mode 100644 index 00000000..83068007 --- /dev/null +++ b/modmail/models/__init__.py @@ -0,0 +1,6 @@ +from .attachments import Attachments # noqa: F401 +from .configurations import Configurations # noqa: F401 +from .embeds import Embeds # noqa: F401 +from .emojis import Emojis # noqa: F401 +from .messages import Messages # noqa: F401 +from .tickets import Tickets # noqa: F401 diff --git a/modmail/models/attachments.py b/modmail/models/attachments.py new file mode 100644 index 00000000..40b385ce --- /dev/null +++ b/modmail/models/attachments.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, ForeignKey, Integer, String + +from modmail.models.base import Base + + +class Attachments(Base): + """ + Database model representing a message attachment sent in a modmail ticket. + + * : Internal ID for the attachment + * : Message ID containing this attachment + * : Discord attachment URL + """ + + internal_id = Column(Integer, primary_key=True) + message_id = Column(Integer, ForeignKey("messages.id")) + link = Column(String) diff --git a/modmail/models/base.py b/modmail/models/base.py new file mode 100644 index 00000000..dd00e200 --- /dev/null +++ b/modmail/models/base.py @@ -0,0 +1,32 @@ +from operator import itemgetter +from typing import Any + +from sqlalchemy.ext.declarative import as_declarative, declared_attr + + +@as_declarative() +class Base: + """ + Base model class. + + Provides functionality + - automatically create table name + - adds `__repr__()` to display model class name and initialisation parameters + """ + + id: Any + __name__: str + + @declared_attr + def __tablename__(cls) -> str: # noqa: N805 + """Generate __tablename__ automatically for a DB model.""" + return cls.__name__.lower() + + def __repr__(self) -> str: + """Returns the current model class name and initialisation parameters.""" + attributes = " ".join( + f"{attribute}={value!r}" + for attribute, value in sorted(self.__dict__.items(), key=itemgetter(0)) + if not attribute.startswith("_") + ) + return f"<{self.__class__.__name__}({attributes})>" diff --git a/modmail/models/configurations.py b/modmail/models/configurations.py new file mode 100644 index 00000000..f9722b03 --- /dev/null +++ b/modmail/models/configurations.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String + +from modmail.models.base import Base + + +class Configurations(Base): + """ + Database model representing a discord modmail bot configurations. + + * : target ID according to the level of the configuration. It can + either be the bot ID or the server ID or a thread ID. If it is a + bot ID: global level, across all its servers. + server ID: server level, acorss all its threads. + thread ID: thread level, for that specific server thread. + * : The configuration name + * : The configuration value + """ + + target_id = Column(Integer, primary_key=True, nullable=False) + config_key = Column(String) + config_value = Column(String) diff --git a/modmail/models/embeds.py b/modmail/models/embeds.py new file mode 100644 index 00000000..966d7c19 --- /dev/null +++ b/modmail/models/embeds.py @@ -0,0 +1,17 @@ +from sqlalchemy import JSON, Column, ForeignKey, Integer + +from modmail.models.base import Base + + +class Embeds(Base): + """ + Database model representing a discord embed. + + * : Internal ID for the embed + * : Message ID containing this embed + * : Embed represented as JSON data + """ + + internal_id = Column(Integer, primary_key=True) + message_id = Column(Integer, ForeignKey("messages.id")) + json_content = Column(JSON) diff --git a/modmail/models/emojis.py b/modmail/models/emojis.py new file mode 100644 index 00000000..a9292a8f --- /dev/null +++ b/modmail/models/emojis.py @@ -0,0 +1,19 @@ +from sqlalchemy import Boolean, Column, Integer, String + +from modmail.models.base import Base + + +class Emojis(Base): + """ + Database model representing a custom discord emoji. + + * : emoji ID for server emojis + * : emoji name + * : discord emoji URL + * : whether the emojis is animated or not + """ + + id = Column(Integer, primary_key=True) + name = Column(String) + url = Column(String) + animated = Column(Boolean, default=False) diff --git a/modmail/models/messages.py b/modmail/models/messages.py new file mode 100644 index 00000000..120a1bd1 --- /dev/null +++ b/modmail/models/messages.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String + +from modmail.models.base import Base + + +class Messages(Base): + """ + Database model representing a message sent in a modmail ticket. + + * : message ID, message IDs are unique + * : modmail ticket ID, the thread created in the guild, + in which this message was sent. + * : mirrored message ID, sent in the guild thread. + * : author ID, would either be the moderator + who sent message in the channel, or the user who has this ticket + opened. + * : message content. + """ + + id = Column(Integer, primary_key=True) + ticket_id = Column(Integer) + mirrored_id = Column(Integer) + author_id = Column(Integer) + content = Column(String) diff --git a/modmail/models/tickets.py b/modmail/models/tickets.py new file mode 100644 index 00000000..3c0847ff --- /dev/null +++ b/modmail/models/tickets.py @@ -0,0 +1,27 @@ +from sqlalchemy import Column, Integer + +from modmail.models.base import Base + + +class Tickets(Base): + """ + An discord modmail ticket for a Discord user with id `creater_id`. + + * : internal ticket ID for modmail, not for users. + * : ticket creation guild. + * : thread created for this ticket. Since threads won't + have the same ID even across guilds, they would be unique so that two + tickets aren't created. + * : ticket opener's discord user ID. + * : message ID which created this ticket, if + it was opened with DMing the bot, it would be that of the DM channel, + if it was opened with running a command on the guild, it would be that. + * : channel ID the thread was created in. + """ + + id = Column(Integer, primary_key=True, unique=True) + server_id = Column(Integer) + thread_id = Column(Integer, unique=True) + creater_id = Column(Integer) + creating_message_id = Column(Integer) + creating_channel_id = Column(Integer) diff --git a/poetry.lock b/poetry.lock index d0fdf060..1adb2159 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,6 +31,22 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotlipy", "cchardet"] +[[package]] +name = "alembic" +version = "1.7.1" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + [[package]] name = "arrow" version = "1.1.1" @@ -50,6 +66,19 @@ category = "main" optional = false python-versions = ">=3.5.3" +[[package]] +name = "asyncpg" +version = "0.24.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] + [[package]] name = "atomicwrites" version = "1.4.0" @@ -269,6 +298,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/master.zip" + [[package]] name = "distlib" version = "0.3.2" @@ -458,6 +488,17 @@ python-versions = ">=3.6" gitdb = ">=4.0.1,<5" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} +[[package]] +name = "greenlet" +version = "1.1.1" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + [[package]] name = "humanfriendly" version = "9.2" @@ -504,6 +545,21 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "importlib-resources" +version = "5.2.2" +description = "Read resources from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -540,6 +596,21 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "mako" +version = "1.1.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + [[package]] name = "markdown" version = "3.3.4" @@ -555,7 +626,7 @@ testing = ["coverage", "pyyaml"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1065,6 +1136,37 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "sqlalchemy" +version = "1.4.22" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\""} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.800)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysqlconnector"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + [[package]] name = "stevedore" version = "3.4.0" @@ -1194,7 +1296,7 @@ multidict = ">=4.0" name = "zipp" version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1205,7 +1307,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "13d426f2143ed7a3fb374ff734603e7dd6e16bf259834232f9fb6c056f11f2bb" +content-hash = "07e8b52b170a00ffa536bb08572fc7a349e9d57b69b5adcc6b972122e697b3c2" [metadata.files] aiodns = [ @@ -1251,6 +1353,10 @@ aiohttp = [ {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.1-py3-none-any.whl", hash = "sha256:25f996b7408b11493d6a2d669fd9d2ff8d87883fe7434182bc7669d6caa526ab"}, + {file = "alembic-1.7.1.tar.gz", hash = "sha256:aea964d3dcc9c205b8759e4e9c1c3935ea3afeee259bffd7ed8414f8085140fb"}, +] arrow = [ {file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"}, {file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"}, @@ -1259,6 +1365,21 @@ 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.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4fc0205fe4ddd5aeb3dfdc0f7bafd43411181e1f5650189608e5971cceacff1"}, + {file = "asyncpg-0.24.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a7095890c96ba36f9f668eb552bb020dddb44f8e73e932f8573efc613ee83843"}, + {file = "asyncpg-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ff5073d4b654e34bd5eaadc01dc4d68b8a9609084d835acd364cd934190a08d"}, + {file = "asyncpg-0.24.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36c6806883786b19551bb70a4882561f31135dc8105a59662e0376cf5b2cbc5"}, + {file = "asyncpg-0.24.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddffcb85227bf39cd1bedd4603e0082b243cf3b14ced64dce506a15b05232b83"}, + {file = "asyncpg-0.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41704c561d354bef01353835a7846e5606faabbeb846214dfcf666cf53319f18"}, + {file = "asyncpg-0.24.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ef6ae0a617fc13cc2ac5dc8e9b367bb83cba220614b437af9b67766f4b6b20"}, + {file = "asyncpg-0.24.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eed43abc6ccf1dc02e0d0efc06ce46a411362f3358847c6b0ec9a43426f91ece"}, + {file = "asyncpg-0.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:129d501f3d30616afd51eb8d3142ef51ba05374256bd5834cec3ef4956a9b317"}, + {file = "asyncpg-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a458fc69051fbb67d995fdda46d75a012b5d6200f91e17d23d4751482640ed4c"}, + {file = "asyncpg-0.24.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:556b0e92e2b75dc028b3c4bc9bd5162ddf0053b856437cf1f04c97f9c6837d03"}, + {file = "asyncpg-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:a738f4807c853623d3f93f0fea11f61be6b0e5ca16ea8aeb42c2c7ee742aa853"}, + {file = "asyncpg-0.24.0.tar.gz", hash = "sha256:dd2fa063c3344823487d9ddccb40802f02622ddf8bf8a6cc53885ee7a2c1c0c6"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1361,11 +1482,6 @@ cffi = [ {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, - {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, - {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, - {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, @@ -1545,6 +1661,58 @@ gitpython = [ {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, ] +greenlet = [ + {file = "greenlet-1.1.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:476ba9435afaead4382fbab8f1882f75e3fb2285c35c9285abb3dd30237f9142"}, + {file = "greenlet-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44556302c0ab376e37939fd0058e1f0db2e769580d340fb03b01678d1ff25f68"}, + {file = "greenlet-1.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40abb7fec4f6294225d2b5464bb6d9552050ded14a7516588d6f010e7e366dcc"}, + {file = "greenlet-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:a11b6199a0b9dc868990456a2667167d0ba096c5224f6258e452bfbe5a9742c5"}, + {file = "greenlet-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e22a82d2b416d9227a500c6860cf13e74060cf10e7daf6695cbf4e6a94e0eee4"}, + {file = "greenlet-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bad269e442f1b7ffa3fa8820b3c3aa66f02a9f9455b5ba2db5a6f9eea96f56de"}, + {file = "greenlet-1.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8ddb38fb6ad96c2ef7468ff73ba5c6876b63b664eebb2c919c224261ae5e8378"}, + {file = "greenlet-1.1.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:84782c80a433d87530ae3f4b9ed58d4a57317d9918dfcc6a59115fa2d8731f2c"}, + {file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac991947ca6533ada4ce7095f0e28fe25d5b2f3266ad5b983ed4201e61596acf"}, + {file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5317701c7ce167205c0569c10abc4bd01c7f4cf93f642c39f2ce975fa9b78a3c"}, + {file = "greenlet-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4870b018ca685ff573edd56b93f00a122f279640732bb52ce3a62b73ee5c4a92"}, + {file = "greenlet-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e0f5e64bcbc6bdbd03774ecb72496224d13b664aa03afd1f9b171a3269272"}, + {file = "greenlet-1.1.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:a414f8e14aa7bacfe1578f17c11d977e637d25383b6210587c29210af995ef04"}, + {file = "greenlet-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e02780da03f84a671bb4205c5968c120f18df081236d7b5462b380fd4f0b497b"}, + {file = "greenlet-1.1.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:dfcb5a4056e161307d103bc013478892cfd919f1262c2bb8703220adcb986362"}, + {file = "greenlet-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:655ab836324a473d4cd8cf231a2d6f283ed71ed77037679da554e38e606a7117"}, + {file = "greenlet-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ce9d0784c3c79f3e5c5c9c9517bbb6c7e8aa12372a5ea95197b8a99402aa0e6"}, + {file = "greenlet-1.1.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3fc6a447735749d651d8919da49aab03c434a300e9f0af1c886d560405840fd1"}, + {file = "greenlet-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8039f5fe8030c43cd1732d9a234fdcbf4916fcc32e21745ca62e75023e4d4649"}, + {file = "greenlet-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fddfb31aa2ac550b938d952bca8a87f1db0f8dc930ffa14ce05b5c08d27e7fd1"}, + {file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97a807437b81f90f85022a9dcfd527deea38368a3979ccb49d93c9198b2c722"}, + {file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf31e894dabb077a35bbe6963285d4515a387ff657bd25b0530c7168e48f167f"}, + {file = "greenlet-1.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eae94de9924bbb4d24960185363e614b1b62ff797c23dc3c8a7c75bbb8d187e"}, + {file = "greenlet-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:c1862f9f1031b1dee3ff00f1027fcd098ffc82120f43041fe67804b464bbd8a7"}, + {file = "greenlet-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9b02e6039eafd75e029d8c58b7b1f3e450ca563ef1fe21c7e3e40b9936c8d03e"}, + {file = "greenlet-1.1.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:84488516639c3c5e5c0e52f311fff94ebc45b56788c2a3bfe9cf8e75670f4de3"}, + {file = "greenlet-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3f8fc59bc5d64fa41f58b0029794f474223693fd00016b29f4e176b3ee2cfd9f"}, + {file = "greenlet-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3e594015a2349ec6dcceda9aca29da8dc89e85b56825b7d1f138a3f6bb79dd4c"}, + {file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e41f72f225192d5d4df81dad2974a8943b0f2d664a2a5cfccdf5a01506f5523c"}, + {file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ff270fd05125dce3303e9216ccddc541a9e072d4fc764a9276d44dee87242b"}, + {file = "greenlet-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cde7ee190196cbdc078511f4df0be367af85636b84d8be32230f4871b960687"}, + {file = "greenlet-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:f253dad38605486a4590f9368ecbace95865fea0f2b66615d121ac91fd1a1563"}, + {file = "greenlet-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a91ee268f059583176c2c8b012a9fce7e49ca6b333a12bbc2dd01fc1a9783885"}, + {file = "greenlet-1.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:34e6675167a238bede724ee60fe0550709e95adaff6a36bcc97006c365290384"}, + {file = "greenlet-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf3725d79b1ceb19e83fb1aed44095518c0fcff88fba06a76c0891cfd1f36837"}, + {file = "greenlet-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5c3b735ccf8fc8048664ee415f8af5a3a018cc92010a0d7195395059b4b39b7d"}, + {file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2002a59453858c7f3404690ae80f10c924a39f45f6095f18a985a1234c37334"}, + {file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e1849c88aa56584d4a0a6e36af5ec7cc37993fdc1fda72b56aa1394a92ded3"}, + {file = "greenlet-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8d4ed48eed7414ccb2aaaecbc733ed2a84c299714eae3f0f48db085342d5629"}, + {file = "greenlet-1.1.1-cp38-cp38-win32.whl", hash = "sha256:2f89d74b4f423e756a018832cd7a0a571e0a31b9ca59323b77ce5f15a437629b"}, + {file = "greenlet-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:d15cb6f8706678dc47fb4e4f8b339937b04eda48a0af1cca95f180db552e7663"}, + {file = "greenlet-1.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b050dbb96216db273b56f0e5960959c2b4cb679fe1e58a0c3906fa0a60c00662"}, + {file = "greenlet-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e0696525500bc8aa12eae654095d2260db4dc95d5c35af2b486eae1bf914ccd"}, + {file = "greenlet-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:07e6d88242e09b399682b39f8dfa1e7e6eca66b305de1ff74ed9eb1a7d8e539c"}, + {file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b491976ed656be9445b79bc57ed21decf08a01aaaf5fdabf07c98c108111f6"}, + {file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e72db813c28906cdc59bd0da7c325d9b82aa0b0543014059c34c8c4ad20e16"}, + {file = "greenlet-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:090126004c8ab9cd0787e2acf63d79e80ab41a18f57d6448225bbfcba475034f"}, + {file = "greenlet-1.1.1-cp39-cp39-win32.whl", hash = "sha256:1796f2c283faab2b71c67e9b9aefb3f201fdfbee5cb55001f5ffce9125f63a45"}, + {file = "greenlet-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4adaf53ace289ced90797d92d767d37e7cdc29f13bd3830c3f0a561277a4ae83"}, + {file = "greenlet-1.1.1.tar.gz", hash = "sha256:c0f22774cd8294078bdf7392ac73cf00bfa1e5e0ed644bd064fdabc5f2a2f481"}, +] humanfriendly = [ {file = "humanfriendly-9.2-py2.py3-none-any.whl", hash = "sha256:332da98c24cc150efcc91b5508b19115209272bfdf4b0764a56795932f854271"}, {file = "humanfriendly-9.2.tar.gz", hash = "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48"}, @@ -1561,6 +1729,10 @@ importlib-metadata = [ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] +importlib-resources = [ + {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"}, + {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1573,17 +1745,31 @@ jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] +mako = [ + {file = "Mako-1.1.5-py2.py3-none-any.whl", hash = "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23"}, + {file = "Mako-1.1.5.tar.gz", hash = "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3"}, +] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1592,14 +1778,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1609,6 +1802,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1970,6 +2166,38 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] +sqlalchemy = [ + {file = "SQLAlchemy-1.4.22-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f"}, + {file = "SQLAlchemy-1.4.22-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0"}, + {file = "SQLAlchemy-1.4.22-cp27-cp27m-win32.whl", hash = "sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39"}, + {file = "SQLAlchemy-1.4.22-cp27-cp27m-win_amd64.whl", hash = "sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397"}, + {file = "SQLAlchemy-1.4.22-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-win32.whl", hash = "sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1"}, + {file = "SQLAlchemy-1.4.22-cp36-cp36m-win_amd64.whl", hash = "sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-win32.whl", hash = "sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11"}, + {file = "SQLAlchemy-1.4.22-cp37-cp37m-win_amd64.whl", hash = "sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-win32.whl", hash = "sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a"}, + {file = "SQLAlchemy-1.4.22-cp38-cp38-win_amd64.whl", hash = "sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-win32.whl", hash = "sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e"}, + {file = "SQLAlchemy-1.4.22-cp39-cp39-win_amd64.whl", hash = "sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9"}, + {file = "SQLAlchemy-1.4.22.tar.gz", hash = "sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8"}, +] stevedore = [ {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"}, {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"}, diff --git a/pyproject.toml b/pyproject.toml index 66e844ae..b5d9fc41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,9 @@ coloredlogs = "^15.0" "discord.py" = { url = "https://github.com/Rapptz/discord.py/archive/master.zip" } pydantic = { version = "^1.8.2", extras = ["dotenv"] } toml = "^0.10.2" - +SQLAlchemy = ">=1.4,<1.4.23" +asyncpg = "^0.24.0" +alembic = "^1.6.5" [tool.poetry.extras] diff --git a/requirements.txt b/requirements.txt index 6ed382e0..c439c6e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,10 @@ aiodns==3.0.0; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" aiohttp==3.7.4.post0; python_version >= "3.6" +alembic==1.7.1; python_version >= "3.6" arrow==1.1.1; python_version >= "3.6" async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" +asyncpg==0.24.0; python_full_version >= "3.6.0" attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" brotlipy==0.7.0; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" cchardet==2.1.7; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" @@ -13,8 +15,12 @@ chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or pyt colorama==0.4.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") coloredlogs==15.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") discord.py @ https://github.com/Rapptz/discord.py/archive/master.zip ; python_full_version >= "3.8.0" +greenlet==1.1.1; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" humanfriendly==9.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" idna==3.2; python_version >= "3.6" +importlib-resources==5.2.2; python_version < "3.9" and python_version >= "3.6" +mako==1.1.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" multidict==5.1.0; python_version >= "3.6" and python_full_version >= "3.8.0" or python_version >= "3.6" pycares==4.0.0; python_version >= "3.6" pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" @@ -23,6 +29,8 @@ pyreadline==2.1; python_version >= "2.7" and python_full_version < "3.0.0" and s python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" python-dotenv==0.19.0; python_full_version >= "3.6.1" and python_version >= "3.5" six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +sqlalchemy==1.4.22; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") -typing-extensions==3.10.0.0; python_version >= "3.6" or python_full_version >= "3.6.1" or python_version >= "3.6" and python_full_version >= "3.8.0" +typing-extensions==3.10.0.2; python_version >= "3.6" or python_full_version >= "3.6.1" or python_version >= "3.6" and python_full_version >= "3.8.0" yarl==1.6.3; python_version >= "3.6" and python_full_version >= "3.8.0" or python_version >= "3.6" +zipp==3.5.0; python_version < "3.9" and python_version >= "3.6" diff --git a/tox.ini b/tox.ini index bc557a04..f84b5afb 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,8 @@ exclude= .md,.svg,.png, venv,.venv, .json, - docs/ + docs/, + modmail/alembic/versions/* ignore= B311,W503,E226,S311,T000, @@ -25,6 +26,8 @@ ignore= ANN002,ANN003,ANN101,ANN102,ANN204,ANN206, # Whitespace Before E203 + # Assert statements are removed when running in optimized mode, but we don't + S101 per-file-ignores= tests/*:,ANN,S101,F401 docs.py:B008