+
+```console
+$ psql -qd postgres
+```
+
+Run the following queries to create the user and database:
+
+```psql
+CREATE USER modmail 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
1. Create a copy of `config-default.yml` named `config.yml` in the the `modmail/` directory.
@@ -182,7 +215,14 @@ $ 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"`.
+ * `DATABASE_URI='postgres://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 `modmail` unless
+you deviated from the setup instructions in the previous section.
### Run The Project
@@ -262,6 +302,37 @@ your chapter weird.
+
+## Applying database migrations
+Migrations are like a version control system for your database. Each migration defines a change to the database
+and how to undo it. By modifying your database through migrations, you create a consistent, testable, and shareable
+way to evolve your databases over time.
+
+You can easily create migrations through aerich's CLI. But before you do that, you need to generate the database
+schemas, this can simply be done by running the bot once (`task start`). Once that's done, you can use the CLI
+to generate the migration raw SQL file:
+
+
+
+```console
+$ aerich migrate
+
+---> 100%
+
+Success migrate 1_202029051520102929_drop_column.sql
+```
+
+
+
+Now, check your `migrations/models` folder. You should see your brand-new migration! Notice that it also
+contains a timestamp. This allows `aerich` to run your migrations in the correct order. Format of migrate filename
+is `{version_num}_{datetime}_{name|update}.sql`. If you are renaming a column, aerich would ask you for confirmation,
+you can then choose `True` to rename the column without a column drop and `False` to drop the column then create.
+Note that the latter may **lose** data.
+
+
+
+
## Changelog Requirement
Modmail has CI that will check for an entry corresponding to your PR in `CHANGES.md`.
diff --git a/migrations/models/0_20211031115120_init.sql b/migrations/models/0_20211031115120_init.sql
new file mode 100644
index 00000000..f80ea37e
--- /dev/null
+++ b/migrations/models/0_20211031115120_init.sql
@@ -0,0 +1,59 @@
+-- upgrade --
+CREATE TABLE IF NOT EXISTS "messages" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "ticket_id" BIGINT NOT NULL,
+ "mirrored_id" BIGINT NOT NULL,
+ "author_id" BIGINT NOT NULL,
+ "content" VARCHAR(4000) NOT NULL
+);
+COMMENT ON TABLE "messages" IS 'Database model representing a message sent in a modmail ticket.';
+CREATE TABLE IF NOT EXISTS "attachments" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "filename" VARCHAR(255) NOT NULL,
+ "file_url" TEXT NOT NULL,
+ "message_id_id" BIGINT NOT NULL REFERENCES "messages" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "attachments" IS 'Database model representing a message attachment sent in a modmail ticket.';
+CREATE TABLE IF NOT EXISTS "embeds" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "content" JSONB NOT NULL,
+ "message_id_id" BIGINT NOT NULL REFERENCES "messages" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "embeds" IS 'Database model representing a discord embed.';
+CREATE TABLE IF NOT EXISTS "emojis" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "name" VARCHAR(32) NOT NULL,
+ "url" TEXT NOT NULL,
+ "animated" BOOL NOT NULL DEFAULT False,
+ "message_id_id" BIGINT NOT NULL REFERENCES "messages" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "emojis" IS 'Database model representing a custom discord emoji.';
+CREATE TABLE IF NOT EXISTS "servers" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "name" VARCHAR(200) NOT NULL,
+ "icon_url" TEXT NOT NULL
+);
+COMMENT ON TABLE "servers" IS 'Database model representing a discord server.';
+CREATE TABLE IF NOT EXISTS "configurations" (
+ "id" SERIAL NOT NULL PRIMARY KEY,
+ "target_bot_id" BIGINT,
+ "config_key" TEXT NOT NULL,
+ "config_value" TEXT NOT NULL,
+ "target_server_id_id" BIGINT REFERENCES "servers" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "configurations" IS 'Database model representing a discord modmail bot configurations.';
+CREATE TABLE IF NOT EXISTS "tickets" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "thread_id" BIGINT NOT NULL UNIQUE,
+ "creater_id" BIGINT NOT NULL,
+ "creating_message_id" BIGINT NOT NULL,
+ "creating_channel_id" BIGINT NOT NULL,
+ "server_id_id" BIGINT NOT NULL REFERENCES "servers" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "tickets" IS 'An discord modmail ticket for a Discord user with id `creator_id`.';
+CREATE TABLE IF NOT EXISTS "aerich" (
+ "id" SERIAL NOT NULL PRIMARY KEY,
+ "version" VARCHAR(255) NOT NULL,
+ "app" VARCHAR(20) NOT NULL,
+ "content" JSONB NOT NULL
+);
diff --git a/migrations/models/1_20211101070158_update.sql b/migrations/models/1_20211101070158_update.sql
new file mode 100644
index 00000000..22a34e81
--- /dev/null
+++ b/migrations/models/1_20211101070158_update.sql
@@ -0,0 +1,10 @@
+-- upgrade --
+CREATE TABLE IF NOT EXISTS "stickers" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "name" VARCHAR(32) NOT NULL,
+ "url" TEXT NOT NULL,
+ "message_id_id" BIGINT NOT NULL REFERENCES "messages" ("id") ON DELETE CASCADE
+);
+COMMENT ON TABLE "stickers" IS 'Database model representing a custom discord sticker.';
+-- downgrade --
+DROP TABLE IF EXISTS "stickers";
diff --git a/migrations/models/2_20211120132336_update.sql b/migrations/models/2_20211120132336_update.sql
new file mode 100644
index 00000000..79dff696
--- /dev/null
+++ b/migrations/models/2_20211120132336_update.sql
@@ -0,0 +1,25 @@
+-- upgrade --
+ALTER TABLE "configurations" DROP CONSTRAINT "fk_configur_servers_471a90ee";
+ALTER TABLE "configurations" RENAME COLUMN "target_server_id_id" TO "target_guild_id_id";
+CREATE TABLE IF NOT EXISTS "guilds" (
+ "id" BIGSERIAL NOT NULL PRIMARY KEY,
+ "name" VARCHAR(200) NOT NULL,
+ "icon_url" TEXT NOT NULL
+);
+COMMENT ON TABLE "guilds" IS 'Database model representing a discord guild.';;
+ALTER TABLE "tickets" ADD "creating_message_id_id" BIGINT NOT NULL;
+ALTER TABLE "tickets" RENAME COLUMN "creater_id" TO "author_id";
+ALTER TABLE "tickets" ADD "author_id" BIGINT NOT NULL;
+ALTER TABLE "tickets" DROP COLUMN "creating_message_id";
+DROP TABLE IF EXISTS "servers";
+ALTER TABLE "configurations" ADD CONSTRAINT "fk_configur_guilds_942a92c3" FOREIGN KEY ("target_guild_id_id") REFERENCES "guilds" ("id") ON DELETE CASCADE;
+ALTER TABLE "tickets" ADD CONSTRAINT "fk_tickets_messages_581a3e15" FOREIGN KEY ("creating_message_id_id") REFERENCES "messages" ("id") ON DELETE CASCADE;
+-- downgrade --
+ALTER TABLE "configurations" DROP CONSTRAINT "fk_configur_guilds_942a92c3";
+ALTER TABLE "tickets" DROP CONSTRAINT "fk_tickets_messages_581a3e15";
+ALTER TABLE "tickets" RENAME COLUMN "author_id" TO "creater_id";
+ALTER TABLE "tickets" RENAME COLUMN "author_id" TO "creating_message_id";
+ALTER TABLE "tickets" DROP COLUMN "creating_message_id_id";
+ALTER TABLE "configurations" RENAME COLUMN "target_guild_id_id" TO "target_server_id_id";
+DROP TABLE IF EXISTS "guilds";
+ALTER TABLE "configurations" ADD CONSTRAINT "fk_configur_servers_471a90ee" FOREIGN KEY ("target_server_id_id") REFERENCES "servers" ("id") ON DELETE CASCADE;
diff --git a/modmail/bot.py b/modmail/bot.py
index de27ea08..eb830c2d 100644
--- a/modmail/bot.py
+++ b/modmail/bot.py
@@ -1,8 +1,8 @@
import asyncio
import logging
import signal
+import sys
import typing as t
-from typing import Any
import arrow
import discord
@@ -10,6 +10,7 @@
from discord import Activity, AllowedMentions, Intents
from discord.client import _cleanup_loop
from discord.ext import commands
+from tortoise import BaseDBAsyncClient, Tortoise
from modmail.config import CONFIG
from modmail.dispatcher import Dispatcher
@@ -27,6 +28,16 @@
emojis_and_stickers=True,
)
+TORTOISE_ORM = {
+ "connections": {"default": CONFIG.bot.database_uri},
+ "apps": {
+ "models": {
+ "models": ["modmail.database.models", "aerich.models"],
+ "default_connection": "default",
+ },
+ },
+}
+
class ModmailBot(commands.Bot):
"""
@@ -65,6 +76,23 @@ def __init__(self, **kwargs):
**kwargs,
)
+ @property
+ def db(self, name: t.Optional[str] = "default") -> BaseDBAsyncClient:
+ """Get the default tortoise-orm connection."""
+ return Tortoise.get_connection(name)
+
+ async def init_db(self) -> None:
+ """Initiate the bot DB connection and check if the DB is alive."""
+ try:
+ self.logger.info("Initializing Tortoise...")
+ await Tortoise.init(TORTOISE_ORM)
+
+ self.logger.info("Generating database schema via Tortoise...")
+ await Tortoise.generate_schemas()
+ except Exception as e:
+ self.logger.error(f"DB connection at {CONFIG.bot.database_uri} not successful, raised:\n{e}")
+ sys.exit(e)
+
async def start(self, token: str, reconnect: bool = True) -> None:
"""
Start the bot.
@@ -73,6 +101,7 @@ async def start(self, token: str, reconnect: bool = True) -> None:
asyncrhonous event loop running, before connecting the bot to discord.
"""
try:
+ await self.init_db()
# create the aiohttp session
self.http_session = ClientSession(loop=self.loop)
self.logger.trace("Created ClientSession.")
@@ -122,7 +151,7 @@ def run(self, *args, **kwargs) -> None:
except NotImplementedError:
pass
- def stop_loop_on_completion(f: Any) -> None:
+ def stop_loop_on_completion(f: t.Any) -> None:
loop.stop()
future = asyncio.ensure_future(self.start(*args, **kwargs), loop=loop)
@@ -167,6 +196,7 @@ async def close(self) -> None:
if self.http_session:
await self.http_session.close()
+ await Tortoise.close_connections()
await super().close()
def load_extensions(self) -> None:
diff --git a/modmail/config.py b/modmail/config.py
index dce1efeb..59aba490 100644
--- a/modmail/config.py
+++ b/modmail/config.py
@@ -97,6 +97,7 @@ def customise_sources(
class BotConfig(BaseSettings):
prefix: str = "?"
token: str = None
+ database_uri: Optional[str] = None
class Config:
# env_prefix = "bot."
diff --git a/modmail/database/__init__.py b/modmail/database/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modmail/database/models/__init__.py b/modmail/database/models/__init__.py
new file mode 100644
index 00000000..2186f80c
--- /dev/null
+++ b/modmail/database/models/__init__.py
@@ -0,0 +1,20 @@
+from .attachments import Attachments
+from .configuration import Configurations
+from .embeds import Embeds
+from .emojis import Emojis
+from .guilds import Guilds
+from .messages import Messages
+from .stickers import Stickers
+from .tickets import Tickets
+
+
+__all__ = (
+ "Attachments",
+ "Configurations",
+ "Embeds",
+ "Emojis",
+ "Guilds",
+ "Messages",
+ "Stickers",
+ "Tickets",
+)
diff --git a/modmail/database/models/attachments.py b/modmail/database/models/attachments.py
new file mode 100644
index 00000000..335f0a11
--- /dev/null
+++ b/modmail/database/models/attachments.py
@@ -0,0 +1,15 @@
+from tortoise import fields
+from tortoise.models import Model
+
+from .messages import Messages
+
+
+class Attachments(Model):
+ """Database model representing a message attachment sent in a modmail ticket."""
+
+ id = fields.BigIntField(pk=True)
+ message_id: fields.ForeignKeyRelation[Messages] = fields.ForeignKeyField(
+ "models.Messages", related_name="attachments", to_field="id"
+ )
+ filename = fields.CharField(max_length=255)
+ file_url = fields.TextField()
diff --git a/modmail/database/models/configuration.py b/modmail/database/models/configuration.py
new file mode 100644
index 00000000..15919f7f
--- /dev/null
+++ b/modmail/database/models/configuration.py
@@ -0,0 +1,21 @@
+from tortoise import fields
+from tortoise.exceptions import ValidationError
+from tortoise.models import Model
+
+from .guilds import Guilds
+
+
+class Configurations(Model):
+ """Database model representing a discord modmail bot configurations."""
+
+ def __init__(self, **kwargs) -> None:
+ if kwargs.get("target_bot_id") and not kwargs.get("target_server_id"):
+ raise ValidationError("`target_bot_id` is mutually exclusive with `target_server_id`.")
+ super().__init__(**kwargs)
+
+ target_bot_id = fields.BigIntField(null=True)
+ target_guild_id: fields.ForeignKeyRelation[Guilds] = fields.ForeignKeyField(
+ "models.Guilds", related_name="configurations", to_field="id", null=True
+ )
+ config_key = fields.TextField()
+ config_value = fields.TextField()
diff --git a/modmail/database/models/embeds.py b/modmail/database/models/embeds.py
new file mode 100644
index 00000000..b0615dbd
--- /dev/null
+++ b/modmail/database/models/embeds.py
@@ -0,0 +1,14 @@
+from tortoise import fields
+from tortoise.models import Model
+
+from .messages import Messages
+
+
+class Embeds(Model):
+ """Database model representing a discord embed."""
+
+ id = fields.BigIntField(pk=True)
+ message_id: fields.ForeignKeyRelation[Messages] = fields.ForeignKeyField(
+ "models.Messages", related_name="embeds", to_field="id"
+ )
+ content = fields.JSONField()
diff --git a/modmail/database/models/emojis.py b/modmail/database/models/emojis.py
new file mode 100644
index 00000000..76b87cef
--- /dev/null
+++ b/modmail/database/models/emojis.py
@@ -0,0 +1,16 @@
+from tortoise import fields
+from tortoise.models import Model
+
+from .messages import Messages
+
+
+class Emojis(Model):
+ """Database model representing a custom discord emoji."""
+
+ id = fields.BigIntField(pk=True)
+ name = fields.CharField(max_length=32)
+ url = fields.TextField()
+ animated = fields.BooleanField(default=False)
+ message_id: fields.ForeignKeyRelation[Messages] = fields.ForeignKeyField(
+ "models.Messages", related_name="emojis", to_field="id"
+ )
diff --git a/modmail/database/models/guilds.py b/modmail/database/models/guilds.py
new file mode 100644
index 00000000..6d1bdaea
--- /dev/null
+++ b/modmail/database/models/guilds.py
@@ -0,0 +1,20 @@
+from typing import TYPE_CHECKING
+
+from tortoise import fields
+from tortoise.models import Model
+
+
+if TYPE_CHECKING:
+ from .configuration import Configurations
+ from .tickets import Tickets
+
+
+class Guilds(Model):
+ """Database model representing a discord guild."""
+
+ id = fields.BigIntField(pk=True, null=False)
+ name = fields.CharField(max_length=200)
+ icon_url = fields.TextField()
+
+ configurations: fields.ReverseRelation["Configurations"]
+ tickets: fields.ReverseRelation["Tickets"]
diff --git a/modmail/database/models/messages.py b/modmail/database/models/messages.py
new file mode 100644
index 00000000..3d3b7947
--- /dev/null
+++ b/modmail/database/models/messages.py
@@ -0,0 +1,28 @@
+from typing import TYPE_CHECKING
+
+from tortoise import fields
+from tortoise.models import Model
+
+
+if TYPE_CHECKING:
+ from .attachments import Attachments
+ from .embeds import Embeds
+ from .emojis import Emojis
+ from .stickers import Stickers
+ from .tickets import Tickets
+
+
+class Messages(Model):
+ """Database model representing a message sent in a modmail ticket."""
+
+ id = fields.BigIntField(pk=True)
+ ticket_id = fields.BigIntField()
+ mirrored_id = fields.BigIntField()
+ author_id = fields.BigIntField()
+ content = fields.CharField(max_length=4000)
+
+ attachments: fields.ReverseRelation["Attachments"]
+ embeds: fields.ReverseRelation["Embeds"]
+ emojis: fields.ReverseRelation["Emojis"]
+ stickers: fields.ReverseRelation["Stickers"]
+ ticket_creations: fields.ReverseRelation["Tickets"]
diff --git a/modmail/database/models/stickers.py b/modmail/database/models/stickers.py
new file mode 100644
index 00000000..276b7654
--- /dev/null
+++ b/modmail/database/models/stickers.py
@@ -0,0 +1,15 @@
+from tortoise import fields
+from tortoise.models import Model
+
+from .messages import Messages
+
+
+class Stickers(Model):
+ """Database model representing a custom discord sticker."""
+
+ id = fields.BigIntField(pk=True)
+ name = fields.CharField(max_length=32)
+ url = fields.TextField()
+ message_id: fields.ForeignKeyRelation[Messages] = fields.ForeignKeyField(
+ "models.Messages", related_name="stickers", to_field="id"
+ )
diff --git a/modmail/database/models/tickets.py b/modmail/database/models/tickets.py
new file mode 100644
index 00000000..f569c979
--- /dev/null
+++ b/modmail/database/models/tickets.py
@@ -0,0 +1,24 @@
+from tortoise import fields
+from tortoise.models import Model
+
+from .guilds import Guilds
+from .messages import Messages
+
+
+class Tickets(Model):
+ """An discord modmail ticket for a Discord user with id `author_id`."""
+
+ id = fields.BigIntField(pk=True, unique=True)
+ server_id: fields.ForeignKeyRelation[Guilds] = fields.ForeignKeyField(
+ "models.Guilds",
+ related_name="tickets",
+ to_field="id",
+ )
+ thread_id = fields.BigIntField(unique=True)
+ author_id = fields.BigIntField()
+ creating_message_id: fields.ForeignKeyRelation[Messages] = fields.ForeignKeyField(
+ "models.Messages",
+ related_name="ticket_creations",
+ to_field="id",
+ )
+ creating_channel_id = fields.BigIntField()
diff --git a/poetry.lock b/poetry.lock
index 71294634..d24b6275 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,22 @@
+[[package]]
+name = "aerich"
+version = "0.5.8"
+description = "A database migrations tool for Tortoise ORM."
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+click = "*"
+ddlparse = "*"
+dictdiffer = "*"
+pydantic = "*"
+tortoise-orm = ">=0.17.7,<0.18.0"
+
+[package.extras]
+aiomysql = ["aiomysql"]
+asyncpg = ["asyncpg"]
+
[[package]]
name = "aiodns"
version = "3.0.0"
@@ -31,6 +50,17 @@ yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotlipy", "cchardet"]
+[[package]]
+name = "aiosqlite"
+version = "0.17.0"
+description = "asyncio bridge to the standard sqlite3 module"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing_extensions = ">=3.7.2"
+
[[package]]
name = "arrow"
version = "1.1.1"
@@ -50,6 +80,19 @@ category = "main"
optional = false
python-versions = ">=3.5.3"
+[[package]]
+name = "asyncpg"
+version = "0.25.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"
@@ -195,7 +238,7 @@ unicode_backport = ["unicodedata2"]
name = "click"
version = "8.0.3"
description = "Composable command line interface toolkit"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -250,6 +293,31 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
[package.extras]
toml = ["tomli"]
+[[package]]
+name = "ddlparse"
+version = "1.10.0"
+description = "DDL parase and Convert to BigQuery JSON schema"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pyparsing = "*"
+
+[[package]]
+name = "dictdiffer"
+version = "0.9.0"
+description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+all = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)", "numpy (>=1.20.0)"]
+docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
+numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
+tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)"]
+
[[package]]
name = "discord.py"
version = "2.0.0a0"
@@ -434,7 +502,7 @@ python-versions = "*"
python-dateutil = ">=2.8.1"
[package.extras]
-dev = ["twine", "markdown", "flake8"]
+dev = ["twine", "markdown", "flake8", "wheel"]
[[package]]
name = "gitdb"
@@ -513,6 +581,14 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "iso8601"
+version = "0.1.16"
+description = "Simple module to parse ISO 8601 dates"
+category = "main"
+optional = false
+python-versions = "*"
+
[[package]]
name = "isort"
version = "5.9.3"
@@ -858,10 +934,18 @@ Markdown = ">=3.2"
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+[[package]]
+name = "pypika-tortoise"
+version = "0.1.1"
+description = "Forked from pypika and streamline just for tortoise-orm"
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+
[[package]]
name = "pyreadline3"
version = "3.3"
@@ -997,6 +1081,14 @@ python-versions = ">=3.5"
[package.extras]
cli = ["click (>=5.0)"]
+[[package]]
+name = "pytz"
+version = "2021.3"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
[[package]]
name = "pyyaml"
version = "5.4.1"
@@ -1128,6 +1220,27 @@ category = "dev"
optional = false
python-versions = ">=3.6"
+[[package]]
+name = "tortoise-orm"
+version = "0.17.8"
+description = "Easy async ORM for python, built with relations in mind"
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+aiosqlite = ">=0.16.0,<0.18.0"
+asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""}
+iso8601 = ">=0.1.13,<0.2.0"
+pypika-tortoise = ">=0.1.1,<0.2.0"
+pytz = "*"
+
+[package.extras]
+aiomysql = ["aiomysql"]
+asyncmy = ["asyncmy"]
+asyncpg = ["asyncpg"]
+accel = ["ciso8601 (>=2.1.2,<3.0.0)", "python-rapidjson", "uvloop (>=0.14.0,<0.15.0)"]
+
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
@@ -1206,9 +1319,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
-content-hash = "b85bebf7a796b0b37e647fd7289589ea03a147c155839dfd1bcd561ead1fde92"
+content-hash = "109b67721fccabddc7388bafe3cf446cbd91970ea571355597c25c0890eb1edb"
[metadata.files]
+aerich = [
+ {file = "aerich-0.5.8-py3-none-any.whl", hash = "sha256:231ea9897f49e1a5e95884cd5fbed31df722dbd417a4778d301e9c46af7457a4"},
+ {file = "aerich-0.5.8.tar.gz", hash = "sha256:1086d67788d805e273b828100fc66593ba96c1baeba7ff1198bf5be993975f4d"},
+]
aiodns = [
{file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"},
{file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"},
@@ -1252,6 +1369,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"},
]
+aiosqlite = [
+ {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
+ {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
+]
arrow = [
{file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"},
{file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"},
@@ -1260,6 +1381,34 @@ 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"},
+]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -1298,6 +1447,14 @@ brotlipy = [
{file = "brotlipy-0.7.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:79aaf217072840f3e9a3b641cccc51f7fc23037496bd71e26211856b93f4b4cb"},
{file = "brotlipy-0.7.0-cp34-cp34m-win32.whl", hash = "sha256:a07647886e24e2fb2d68ca8bf3ada398eb56fd8eac46c733d4d95c64d17f743b"},
{file = "brotlipy-0.7.0-cp34-cp34m-win_amd64.whl", hash = "sha256:c6cc0036b1304dd0073eec416cb2f6b9e37ac8296afd9e481cac3b1f07f9db25"},
+ {file = "brotlipy-0.7.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:382971a641125323e90486244d6266ffb0e1f4dd920fbdcf508d2a19acc7c3b3"},
+ {file = "brotlipy-0.7.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:82f61506d001e626ec3a1ac8a69df11eb3555a4878599befcb672c8178befac8"},
+ {file = "brotlipy-0.7.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:7ff18e42f51ebc9d9d77a0db33f99ad95f01dd431e4491f0eca519b90e9415a9"},
+ {file = "brotlipy-0.7.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:8ef230ca9e168ce2b7dc173a48a0cc3d78bcdf0bd0ea7743472a317041a4768e"},
+ {file = "brotlipy-0.7.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:b7cf5bb69e767a59acc3da0d199d4b5d0c9fed7bef3ffa3efa80c6f39095686b"},
+ {file = "brotlipy-0.7.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:e5c549ae5928dda952463196180445c24d6fad2d73cb13bd118293aced31b771"},
+ {file = "brotlipy-0.7.0-cp35-abi3-win32.whl", hash = "sha256:79ab3bca8dd12c17e092273484f2ac48b906de2b4828dcdf6a7d520f99646ab3"},
+ {file = "brotlipy-0.7.0-cp35-abi3-win_amd64.whl", hash = "sha256:ac1d66c9774ee62e762750e399a0c95e93b180e96179b645f28b162b55ae8adc"},
{file = "brotlipy-0.7.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:07194f4768eb62a4f4ea76b6d0df6ade185e24ebd85877c351daa0a069f1111a"},
{file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7e31f7adcc5851ca06134705fcf3478210da45d35ad75ec181e1ce9ce345bb38"},
{file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9448227b0df082e574c45c983fa5cd4bda7bfb11ea6b59def0940c1647be0c3c"},
@@ -1309,6 +1466,7 @@ brotlipy = [
{file = "brotlipy-0.7.0-cp36-cp36m-win32.whl", hash = "sha256:2e5c64522364a9ebcdf47c5744a5ddeb3f934742d31e61ebfbbc095460b47162"},
{file = "brotlipy-0.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:09ec3e125d16749b31c74f021aba809541b3564e5359f8c265cbae442810b41a"},
{file = "brotlipy-0.7.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:4e4638b49835d567d447a2cfacec109f9a777f219f071312268b351b6839436d"},
+ {file = "brotlipy-0.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5664fe14f3a613431db622172bad923096a303d3adce55536f4409c8e2eafba4"},
{file = "brotlipy-0.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1379347337dc3d20b2d61456d44ccce13e0625db2611c368023b4194d5e2477f"},
{file = "brotlipy-0.7.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:22a53ccebcce2425e19f99682c12be510bf27bd75c9b77a1720db63047a77554"},
{file = "brotlipy-0.7.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4bac11c1ffba9eaa2894ec958a44e7f17778b3303c2ee9f99c39fcc511c26668"},
@@ -1470,6 +1628,14 @@ coverage = [
{file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
{file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
]
+ddlparse = [
+ {file = "ddlparse-1.10.0-py3-none-any.whl", hash = "sha256:71761b3457c8720853af3aeef266e2da1b6edef50936969492d586d7046a2ac2"},
+ {file = "ddlparse-1.10.0.tar.gz", hash = "sha256:6418681baa848eb01251ab79eb3d0ad7e140e6ab1deaae5a019353ddb3a908da"},
+]
+dictdiffer = [
+ {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"},
+ {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"},
+]
"discord.py" = []
distlib = [
{file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"},
@@ -1523,6 +1689,7 @@ flake8-todo = [
]
ghp-import = [
{file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"},
+ {file = "ghp_import-2.0.1-py3-none-any.whl", hash = "sha256:8241a8e9f8dd3c1fafe9696e6e081b57a208ef907e9939c44e7415e407ab40ea"},
]
gitdb = [
{file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"},
@@ -1552,6 +1719,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
+iso8601 = [
+ {file = "iso8601-0.1.16-py2.py3-none-any.whl", hash = "sha256:906714829fedbc89955d52806c903f2332e3948ed94e31e85037f9e0226b8376"},
+ {file = "iso8601-0.1.16.tar.gz", hash = "sha256:36532f77cc800594e8f16641edae7f1baf7932f05d8e508545b95fc53c6dc85b"},
+]
isort = [
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
{file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
@@ -1877,6 +2048,10 @@ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
+pypika-tortoise = [
+ {file = "pypika-tortoise-0.1.1.tar.gz", hash = "sha256:6831d0a56e5e0ecefac3307dd9bdb3e5073fdac5d617401601d3a6713e059f3c"},
+ {file = "pypika_tortoise-0.1.1-py3-none-any.whl", hash = "sha256:860020094e01058ea80602c90d4a843d0a42cffefcf4f3cb1a7f2c18b880c638"},
+]
pyreadline3 = [
{file = "pyreadline3-3.3-py3-none-any.whl", hash = "sha256:0003fd0079d152ecbd8111202c5a7dfa6a5569ffd65b235e45f3c2ecbee337b4"},
{file = "pyreadline3-3.3.tar.gz", hash = "sha256:ff3b5a1ac0010d0967869f723e687d42cabc7dccf33b14934c92aa5168d260b3"},
@@ -1915,6 +2090,10 @@ python-dotenv = [
{file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"},
{file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"},
]
+pytz = [
+ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
+ {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
+]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -2032,6 +2211,10 @@ tomli = [
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
]
+tortoise-orm = [
+ {file = "tortoise-orm-0.17.8.tar.gz", hash = "sha256:1f5020e9964d32a4d6ed685d466b5d7285de328a63ee92ee988c1e4baf8fefbf"},
+ {file = "tortoise_orm-0.17.8-py3-none-any.whl", hash = "sha256:f18c41bb83be4748a6ca259ed7309ca954b35f5790971824bbc79a11d2b1ef3b"},
+]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
diff --git a/pyproject.toml b/pyproject.toml
index 245c8fad..d7e6a1a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,8 @@ 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"
+tortoise-orm = {extras = ["asyncpg"], version = "^0.17.8"}
+aerich = "^0.5.8"
[tool.poetry.extras]
diff --git a/requirements.txt b/requirements.txt
index d0251ef6..e48a68b2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,28 +1,39 @@
# NOTICE: This file is automatically generated by scripts/export_requirements.py
# This is also automatically regenerated when an edit to pyproject.toml or poetry.lock is commited.
+aerich==0.5.8 ; python_version >= "3.7"
aiodns==3.0.0
aiohttp==3.7.4.post0 ; python_version >= "3.6"
+aiosqlite==0.17.0 ; python_version >= "3.6"
arrow==1.1.1 ; python_version >= "3.6"
async-timeout==3.0.1 ; python_full_version >= "3.5.3"
+asyncpg==0.25.0 ; python_full_version >= "3.6.0"
attrs==21.2.0 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4"
brotlipy==0.7.0
cchardet==2.1.7
cffi==1.15.0
chardet==4.0.0 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4"
+click==8.0.3 ; python_version >= "3.6"
colorama==0.4.4 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4"
coloredlogs==15.0.1 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4"
+ddlparse==1.10.0
+dictdiffer==0.9.0
discord.py @ https://github.com/Rapptz/discord.py/archive/master.zip ; python_full_version >= "3.8.0"
humanfriendly==10.0 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4"
idna==3.2 ; python_version >= "3.5"
+iso8601==0.1.16
multidict==5.2.0 ; python_version >= "3.6"
pycares==4.1.2
pycparser==2.20 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3"
pydantic==1.8.2 ; python_full_version >= "3.6.1"
+pyparsing==2.4.7 ; python_version >= "2.6" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2"
+pypika-tortoise==0.1.1 ; python_version >= "3.7"
pyreadline3==3.3 ; sys_platform == "win32"
python-dateutil==2.8.2 ; python_version != "3.0"
python-dotenv==0.19.0 ; python_version >= "3.5"
+pytz==2021.3
six==1.16.0 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2"
toml==0.10.2 ; python_version >= "2.6" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2"
+tortoise-orm==0.17.8 ; python_version >= "3.7"
typing-extensions==3.10.0.2
yarl==1.7.2 ; python_version >= "3.6"
diff --git a/tox.ini b/tox.ini
index bc557a04..a97152d4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,6 +28,7 @@ ignore=
per-file-ignores=
tests/*:,ANN,S101,F401
docs.py:B008
+ modmail/database/models/__init__.py:F401
[isort]
profile=black