feat(aurora): migrated to aiosqlite

This commit is contained in:
Seaswimmer 2024-06-05 00:14:43 -04:00
parent 027144f35d
commit 5cbf4e7e47
Signed by untrusted user: cswimr
GPG key ID: 5D671B5D03D65A7F
9 changed files with 127 additions and 116 deletions

View file

@ -7,11 +7,11 @@
import json
import os
import sqlite3
import time
from datetime import datetime, timedelta, timezone
from math import ceil
import aiosqlite
import discord
from discord import Object
from discord.ext import tasks
@ -50,21 +50,21 @@ class Aurora(commands.Cog):
if requester == "discord_deleted_user":
await config.user_from_id(user_id).clear()
database = connect()
cursor = database.cursor()
database = await connect()
cursor = await database.cursor()
cursor.execute("SHOW TABLES;")
await cursor.execute("SHOW TABLES;")
tables = [table[0] for table in cursor.fetchall()]
condition = "target_id = %s OR moderator_id = %s;"
for table in tables:
delete_query = f"DELETE FROM {table[0]} WHERE {condition}"
cursor.execute(delete_query, (user_id, user_id))
await cursor.execute(delete_query, (user_id, user_id))
database.commit()
cursor.close()
database.close()
await database.commit()
await cursor.close()
await database.close()
if requester == "owner":
await config.user_from_id(user_id).clear()
if requester == "user":
@ -121,15 +121,13 @@ class Aurora(commands.Cog):
async def addrole_on_member_join(self, member: discord.Member):
"""This method automatically adds roles to users when they join the server."""
if not await self.bot.cog_disabled_in_guild(self, member.guild):
query = f"""SELECT moderation_id, role_id, reason FROM moderation_{member.guild.id} WHERE target_id = ? AND moderation_type = 'ADDROLE' AND expired = 0 AND resolved = 0;"""
database = connect()
cursor = database.cursor()
cursor.execute(query, (member.id,))
results = cursor.fetchall()
for result in results:
role = member.guild.get_role(result[1])
reason = result[2]
await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{result[0]:,})")
async with connect() as database:
query = f"""SELECT moderation_id, role_id, reason FROM moderation_{member.guild.id} WHERE target_id = ? AND moderation_type = 'ADDROLE' AND expired = 0 AND resolved = 0;"""
async with database.execute(query, (member.id,)) as cursor:
async for row in cursor:
role = member.guild.get_role(row[1])
reason = row[2]
await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{row[0]:,})")
@commands.Cog.listener("on_audit_log_entry_create")
async def autologger(self, entry: discord.AuditLogEntry):
@ -175,7 +173,7 @@ class Aurora(commands.Cog):
else:
return
Moderation.log(
await Moderation.log(
self.bot,
entry.guild.id,
entry.user.id,
@ -233,7 +231,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException:
pass
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -292,7 +290,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException:
pass
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -398,7 +396,7 @@ class Aurora(commands.Cog):
content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
)
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -504,7 +502,7 @@ class Aurora(commands.Cog):
content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
)
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -592,7 +590,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException:
pass
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -728,7 +726,7 @@ class Aurora(commands.Cog):
await target.kick(reason=f"Kicked by {interaction.user.id} for: {reason}")
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -836,7 +834,7 @@ class Aurora(commands.Cog):
delete_message_seconds=delete_messages_seconds,
)
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -880,7 +878,7 @@ class Aurora(commands.Cog):
delete_message_seconds=delete_messages_seconds,
)
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -957,7 +955,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException:
pass
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -1001,7 +999,7 @@ class Aurora(commands.Cog):
await channel.edit(slowmode_delay=interval)
await interaction.response.send_message(f"Slowmode in {channel.mention} has been set to {interval} seconds!\n**Reason** - `{reason}`")
moderation = Moderation.log(
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
@ -1089,7 +1087,7 @@ class Aurora(commands.Cog):
)
return
database = connect()
database = await connect()
if export:
try:
@ -1099,7 +1097,7 @@ class Aurora(commands.Cog):
+ f"moderation_{interaction.guild.id}.json"
)
cases = Moderation.get_latest(bot=interaction.client, guild_id=interaction.guild.id)
cases = await Moderation.get_latest(bot=interaction.client, guild_id=interaction.guild.id)
with open(filename, "w", encoding="utf-8") as f:
dump(obj=cases, fp=f, indent=2)
@ -1120,15 +1118,15 @@ class Aurora(commands.Cog):
+ box(e, "py"),
ephemeral=ephemeral,
)
database.close()
await database.close()
return
if target:
moderations = Moderation.find_by_target(interaction.client, interaction.guild.id, target.id)
moderations = await Moderation.find_by_target(interaction.client, interaction.guild.id, target.id)
elif moderator:
moderations = Moderation.find_by_moderator(interaction.client, interaction.guild.id, moderator.id)
moderations = await Moderation.find_by_moderator(interaction.client, interaction.guild.id, moderator.id)
else:
moderations = Moderation.get_latest(interaction.client, interaction.guild.id)
moderations = await Moderation.get_latest(interaction.client, interaction.guild.id)
case_quantity = len(moderations)
page_quantity = ceil(case_quantity / pagesize)
@ -1214,7 +1212,7 @@ class Aurora(commands.Cog):
return
try:
moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id)
moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError:
await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True
@ -1298,7 +1296,7 @@ class Aurora(commands.Cog):
)
try:
mod = Moderation.find_by_id(interaction.client, case, interaction.guild.id)
mod = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError:
await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True
@ -1391,7 +1389,7 @@ class Aurora(commands.Cog):
return
try:
moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id)
moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
old_moderation = moderation
except ValueError:
await interaction.response.send_message(
@ -1469,7 +1467,7 @@ class Aurora(commands.Cog):
"end_timestamp": moderation.end_timestamp,
}))
moderation.update()
await moderation.update()
embed = await case_factory(interaction=interaction, moderation=moderation)
await interaction.response.send_message(
@ -1485,8 +1483,8 @@ class Aurora(commands.Cog):
async def handle_expiry(self):
await self.bot.wait_until_red_ready()
current_time = time.time()
database = connect()
cursor = database.cursor()
database = await connect()
cursor = await database.cursor()
global_unban_num = 0
global_addrole_num = 0
global_removerole_num = 0
@ -1499,9 +1497,9 @@ class Aurora(commands.Cog):
tempban_query = f"SELECT target_id, moderation_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'TEMPBAN' AND expired = 0"
try:
cursor.execute(tempban_query, (time.time(),))
await cursor.execute(tempban_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
except aiosqlite.OperationalError:
continue
target_ids = [row[0] for row in result]
@ -1558,9 +1556,9 @@ class Aurora(commands.Cog):
removerole_num = 0
addrole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'ADDROLE' AND expired = 0"
try:
cursor.execute(addrole_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
await cursor.execute(addrole_query, (time.time(),))
result = await cursor.fetchall()
except aiosqlite.OperationalError:
continue
target_ids = [row[0] for row in result]
moderation_ids = [row[1] for row in result]
@ -1593,9 +1591,9 @@ class Aurora(commands.Cog):
addrole_num = 0
removerole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'REMOVEROLE' AND expired = 0"
try:
cursor.execute(removerole_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
await cursor.execute(removerole_query, (time.time(),))
result = await cursor.fetchall()
except aiosqlite.OperationalError:
continue
target_ids = [row[0] for row in result]
moderation_ids = [row[1] for row in result]
@ -1621,7 +1619,7 @@ class Aurora(commands.Cog):
continue
expiry_query = f"UPDATE `moderation_{guild.id}` SET expired = 1 WHERE (end_timestamp IS NOT NULL AND end_timestamp <= ? AND expired = 0) OR (expired = 0 AND resolved = 1);"
cursor.execute(expiry_query, (time.time(),))
await cursor.execute(expiry_query, (time.time(),))
per_guild_completion_time = (time.time() - time_per_guild) * 1000
logger.debug(
@ -1637,9 +1635,9 @@ class Aurora(commands.Cog):
global_addrole_num = global_addrole_num + addrole_num
global_removerole_num = global_removerole_num + removerole_num
database.commit()
cursor.close()
database.close()
await database.commit()
await cursor.close()
await database.close()
completion_time = (time.time() - current_time) * 1000
logger.debug(

View file

@ -27,14 +27,10 @@ class ImportAuroraView(ui.View):
"Deleting original table...", ephemeral=True
)
database = connect()
cursor = database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query)
cursor.close()
database.commit()
async with connect() as database:
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
database.execute(query)
database.commit()
await interaction.edit_original_response(content="Creating new table...")
@ -96,7 +92,7 @@ class ImportAuroraView(ui.View):
duration = None
try:
Moderation.log(
await Moderation.log(
bot=interaction.client,
guild_id=self.ctx.guild.id,
moderator_id=case["moderator_id"],

View file

@ -26,14 +26,14 @@ class ImportGalacticBotView(ui.View):
"Deleting original table...", ephemeral=True
)
database = connect()
cursor = database.cursor()
database = await connect()
cursor = await database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query)
await cursor.execute(query)
cursor.close()
database.commit()
await cursor.close()
await database.commit()
await interaction.edit_original_response(content="Creating new table...")
@ -124,7 +124,7 @@ class ImportGalacticBotView(ui.View):
else:
reason = None
Moderation.log(
await Moderation.log(
self.ctx.guild.id,
case["executor"],
case["type"],

View file

@ -9,7 +9,7 @@
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["pydantic"],
"requirements": ["pydantic", "aiosqlite"],
"tags": [
"mod",
"moderate",

View file

@ -1,11 +1,11 @@
import json
import sqlite3
from datetime import datetime, timedelta
from sqlite3 import Cursor
from time import time
from typing import Dict, Iterable, List, Optional, Tuple, Union
import discord
from aiosqlite import Cursor
from discord import NotFound
from redbot.core.bot import Red
@ -115,15 +115,15 @@ class Moderation(AuroraGuildModel):
"user_id": resolved_by,
}))
self.update()
await self.update()
def update(self) -> None:
async def update(self) -> None:
from ..utilities.database import connect
from ..utilities.json import dumps
query = f"UPDATE moderation_{self.guild_id} SET timestamp = ?, moderation_type = ?, target_type = ?, moderator_id = ?, role_id = ?, duration = ?, end_timestamp = ?, reason = ?, resolved = ?, resolved_by = ?, resolve_reason = ?, expired = ?, changes = ?, metadata = ? WHERE moderation_id = ?;"
with connect() as database:
database.execute(query, (
async with connect() as database:
await database.execute(query, (
self.timestamp.timestamp(),
self.moderation_type,
self.target_type,
@ -206,7 +206,7 @@ class Moderation(AuroraGuildModel):
return cls.from_dict(bot=bot, data=case)
@classmethod
def execute(cls, bot: Red, guild_id: int, query: str, parameters: tuple | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
async def execute(cls, bot: Red, guild_id: int, query: str, parameters: tuple | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
from ..utilities.database import connect
logger.trace("Executing query: %s", query)
logger.trace("With parameters: %s", parameters)
@ -214,16 +214,16 @@ class Moderation(AuroraGuildModel):
parameters = ()
if not cursor:
no_cursor = True
database = connect()
cursor = database.cursor()
database = await connect()
cursor = await database.cursor()
else:
no_cursor = False
cursor.execute(query, parameters)
results = cursor.fetchall()
await cursor.execute(query, parameters)
results = await cursor.fetchall()
if no_cursor:
cursor.close()
database.close()
await cursor.close()
await database.close()
if results:
cases = []
@ -235,7 +235,7 @@ class Moderation(AuroraGuildModel):
return ()
@classmethod
def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
async def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
params = []
query = f"SELECT * FROM moderation_{guild_id} ORDER BY moderation_id DESC"
if types:
@ -245,41 +245,41 @@ class Moderation(AuroraGuildModel):
query += " LIMIT ? OFFSET ?"
params.extend((limit, offset))
query += ";"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor)
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor)
@classmethod
def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int:
result = cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1)
async def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int:
result = await cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1)
return (result[0].moderation_id + 1) if result else 1
@classmethod
def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation":
async def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation":
query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;"
case = cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor)
case = await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor)
if case:
return case[0]
raise ValueError(f"Case {moderation_id} not found in moderation_{guild_id}!")
@classmethod
def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
async def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE target_id = ?"
if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
query += " ORDER BY moderation_id DESC;"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *types) if types else (target,), cursor=cursor)
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *types) if types else (target,), cursor=cursor)
@classmethod
def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
async def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE moderator_id = ?"
if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
query += " ORDER BY moderation_id DESC;"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *types) if types else (moderator,), cursor=cursor)
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *types) if types else (moderator,), cursor=cursor)
@classmethod
def log(
async def log(
cls,
bot: Red,
guild_id: int,
@ -335,13 +335,12 @@ class Moderation(AuroraGuildModel):
role_id = None
if not database:
database = connect()
database = await connect()
close_db = True
else:
close_db = False
cursor = database.cursor()
moderation_id = cls.get_next_case_number(bot=bot, guild_id=guild_id, cursor=cursor)
moderation_id = cls.get_next_case_number(bot=bot, guild_id=guild_id)
case = {
"moderation_id": moderation_id,
@ -363,12 +362,12 @@ class Moderation(AuroraGuildModel):
}
sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
cursor.execute(sql, tuple(case.values()))
await database.execute(sql, tuple(case.values()))
cursor.close()
database.commit()
await database.close()
await database.commit()
if close_db:
database.close()
await database.close()
logger.debug(
"Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
@ -392,5 +391,5 @@ class Moderation(AuroraGuildModel):
)
if return_obj:
return cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id)
return await cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id)
return moderation_id

View file

@ -1,22 +1,22 @@
# pylint: disable=cyclic-import
import json
import sqlite3
import aiosqlite
from discord import Guild
from redbot.core import data_manager
from .logger import logger
def connect() -> sqlite3.Connection:
async def connect() -> aiosqlite.Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = sqlite3.connect(
connection = await aiosqlite.connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
except sqlite3.OperationalError as e:
except aiosqlite.OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
@ -24,14 +24,13 @@ def connect() -> sqlite3.Connection:
async def create_guild_table(guild: Guild):
database = connect()
cursor = database.cursor()
database = await connect()
try:
cursor.execute(f"SELECT * FROM `moderation_{guild.id}`")
await database.execute(f"SELECT * FROM `moderation_{guild.id}`")
logger.debug("SQLite Table exists for server %s (%s)", guild.name, guild.id)
except sqlite3.OperationalError:
except aiosqlite.OperationalError:
query = f"""
CREATE TABLE `moderation_{guild.id}` (
moderation_id INTEGER PRIMARY KEY,
@ -52,16 +51,16 @@ async def create_guild_table(guild: Guild):
metadata JSON NOT NULL
)
"""
cursor.execute(query)
await database.execute(query)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
cursor.execute(index_query_1)
await database.execute(index_query_1)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
cursor.execute(index_query_2)
await database.execute(index_query_2)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
cursor.execute(index_query_3)
await database.execute(index_query_3)
insert_query = f"""
INSERT INTO `moderation_{guild.id}`
@ -86,9 +85,9 @@ async def create_guild_table(guild: Guild):
json.dumps([]),
json.dumps({}),
)
cursor.execute(insert_query, insert_values)
await database.execute(insert_query, insert_values)
database.commit()
await database.commit()
logger.debug(
"SQLite Table (moderation_%s) created for %s (%s)",

View file

@ -120,7 +120,7 @@ async def log(interaction: Interaction, moderation_id: int, resolved: bool = Fal
logging_channel = interaction.guild.get_channel(logging_channel_id)
try:
moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild_id)
moderation = await Moderation.find_by_id(interaction.client, moderation_id, interaction.guild_id)
embed = await log_factory(
interaction=interaction, moderation=moderation, resolved=resolved
)
@ -145,7 +145,7 @@ async def send_evidenceformat(interaction: Interaction, moderation_id: int) -> N
if send_evidence_bool is False:
return
moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild.id)
moderation = await Moderation.find_by_id(interaction.client, moderation_id, interaction.guild.id)
content = await evidenceformat_factory(moderation=moderation)
await interaction.followup.send(content=content, ephemeral=True)

20
poetry.lock generated
View file

@ -123,6 +123,24 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "aiosqlite"
version = "0.20.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
]
[package.dependencies]
typing_extensions = ">=4.0"
[package.extras]
dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"]
docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"]
[[package]]
name = "annotated-types"
version = "0.7.0"
@ -2655,4 +2673,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.12"
content-hash = "3f732c0b0b0eb2a31fb9484c7cf699cd3c26474d6779528102af4c509a48351e"
content-hash = "22b824824f73dc3dc1a9a0a01060371ee1f6414e5bef39cb7455d21121988b47"

View file

@ -18,6 +18,7 @@ pydantic = "^2.7.1"
colorthief = "^0.2.1"
beautifulsoup4 = "^4.12.3"
markdownify = "^0.12.1"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev]
optional = true