Compare commits

...
34 Commits
Author SHA1 Message Date
imcompiling 9550d1a68b Remove all pylint supressors. 2025-03-03 23:41:58 -05:00
imcompiling b04819aa36 Fix smells and model usage in guild listener. 2025-03-03 23:40:49 -05:00
imcompiling f43dccb12d Finish fixing lint smells in loops directory. 2025-03-03 23:36:07 -05:00
imcompiling 9cf05f44d0 Fix error in model.
The two channel ids need to be NULL by default, don't know them on object creation.
2025-03-03 23:32:03 -05:00
imcompiling f6ae34c9e6 Started fixing smells and updating model calls. 2025-03-03 23:29:46 -05:00
imcompiling 528af14e00 Update model usage in verification view. 2025-03-03 23:14:31 -05:00
imcompiling dbe652dcce Fix smells in submission init file. 2025-03-03 23:08:53 -05:00
imcompiling 1658638938 Remove unneeded parenthesis. 2025-03-03 23:08:41 -05:00
imcompiling 43f51f7f07 Test file after connecting to prod database and testing relationships. 2025-03-03 23:02:40 -05:00
imcompiling f74b08b02e Add another word to cspell. 2025-03-03 23:02:19 -05:00
imcompiling 9dd07bef93 Add an init file for utilities. 2025-03-03 23:01:58 -05:00
imcompiling 231c700946 Fix other errors with models. 2025-03-03 22:57:48 -05:00
imcompiling 37daa0e657 Update names used in constraints. 2025-03-03 22:46:33 -05:00
imcompiling 6dfd3acad0 Add SnakeCase column names that are already in database. 2025-03-03 22:44:14 -05:00
imcompiling ce3ed3c001 Just more testing. 2025-03-03 21:53:19 -05:00
imcompiling 0ad425b982 Small lint adjustments. 2025-03-03 21:52:54 -05:00
imcompiling cf5629a6dd Finish updating utils to use new models. 2025-03-03 21:52:30 -05:00
imcompiling 37fb9c1abb Use col to correct type hint errors. 2025-03-03 21:01:59 -05:00
imcompiling 157b8d4f3f Begin updating utils. 2025-03-03 15:54:28 -05:00
imcompiling 1c3fb231b7 Add more cspell words. 2025-03-03 15:54:20 -05:00
imcompiling b72dd35e04 Add init file to the database package. 2025-03-03 15:28:00 -05:00
imcompiling 0b2a1cd293 Organize import in db.py. 2025-03-03 15:19:46 -05:00
imcompiling f1f0c0c6de Rename database_setup directory to database. 2025-03-03 15:18:31 -05:00
imcompiling 36eae17751 Update db.py just contain engine and create database. 2025-03-03 15:16:24 -05:00
imcompiling 7a57b75ca3 Add comments to models. 2025-03-03 14:41:24 -05:00
imcompiling 0ea9ef5b41 Rename sql_data_classes to db. 2025-03-03 14:29:50 -05:00
imcompiling e13790ba08 Add rest of docstrings. 2025-03-03 14:27:26 -05:00
imcompiling 171a1b0840 Move sql models into their own file. 2025-03-03 14:21:20 -05:00
imcompiling bd365d575f Add relationships. 2025-02-28 16:12:09 -05:00
imcompiling 0e262d5972 Add the rest of the tables as SQLModels. 2025-02-28 16:00:58 -05:00
imcompiling 774e6ffedf Add CategoryGroup definition.
Not working completely, the unique constraint is missing.
2025-02-27 16:28:00 -05:00
imcompiling de2542ece6 Start testing with sqlmodel. 2025-02-27 09:42:49 -05:00
imcompiling c29194a30a Install sqlmodel 2025-02-26 21:35:25 -05:00
imcompiling 8870d93a44 Fix quick code smell. 2025-02-26 20:42:44 -05:00
29 changed files with 453 additions and 176 deletions
+1
View File
@@ -135,3 +135,4 @@ deleted_backups/
highscores_data/
TODO.txt
sql_tester.py
database.db
+6 -1
View File
@@ -1,7 +1,12 @@
{
"cSpell.words": [
"dotenv",
"gmtime",
"highscore",
"highscores"
"highscores",
"imcompiling",
"nextcord",
"ondelete"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportOptionalMemberAccess": false,
+6 -5
View File
@@ -5,7 +5,7 @@ from nextcord.ext import commands
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Category, CategoryGroup, engine
from database.db import Category, CategoryGroup, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
@@ -97,10 +97,9 @@ class CreateCog(commands.Cog, name="Create Cog"):
min_value=1,
max_value=30,
),
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
channel: nextcord.TextChannel | None = nextcord.SlashOption(
description="Channel to post Group in. If left empty, defaults to current channel.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
@@ -119,11 +118,13 @@ class CreateCog(commands.Cog, name="Create Cog"):
"""
# if no channel provided, use current channel.
if channel is None:
channel = interaction.channel
channel_id: int = interaction.channel.id
else:
channel_id: int = channel.id
new_group = CategoryGroup(
GroupName=group_name,
ChannelId=channel.id,
ChannelId=channel_id,
GuildId=interaction.guild_id,
PadLength=pad_length,
UsernameLength=username_length,
+1 -2
View File
@@ -5,8 +5,7 @@ from nextcord.ext import commands
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from database.db import Category, CategoryGroup, Score, engine
from utilities.utils import (
autofill_categories,
autofill_groups,
+1 -2
View File
@@ -7,8 +7,7 @@ from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import CategoryGroup, engine
from database.db import CategoryGroup, engine
from utilities.utils import autofill_groups, send_highscore_message
+1 -2
View File
@@ -7,8 +7,7 @@ from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import CategoryGroup, Guild, engine
from database.db import CategoryGroup, Guild, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from cogs.submission.verification_view import VerificationView
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, Submission, engine
from database.db import Category, CategoryGroup, Guild, Submission, engine
from main import logger
from utilities.utils import autofill_categories, autofill_groups, submit_score
+1 -2
View File
@@ -7,8 +7,7 @@ from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
from database.db import Guild, engine
from utilities.utils import autofill_users_in_guild
from .views.update_username_verify import UpdateUsernameVerifyView
+1 -2
View File
@@ -6,8 +6,7 @@ from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from database.db import Category, CategoryGroup, Score, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
+1 -2
View File
@@ -6,8 +6,7 @@ import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, engine
from database.db import Category, CategoryGroup, engine
from utilities.utils import send_highscore_message
@@ -4,8 +4,7 @@ import nextcord
from sqlalchemy import distinct, exc, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from database.db import Category, CategoryGroup, Score, engine
from utilities.utils import send_highscore_message
@@ -19,7 +18,7 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
super().__init__(timeout=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-username-update")
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): # pylint: disable=unused-argument
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Button for denying username update. Deletes message.
@@ -30,7 +29,7 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-username-update")
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): # pylint: disable=unused-argument
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Button for approving username update. Tells guild data to update username, then updates
message to show it is complete.
+11 -12
View File
@@ -1,14 +1,13 @@
"""
Defines cog for guild listeners. Contains functions for various guild events.
"""
"""Defines cog for guild listeners. Contains functions for various guild events."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
from database.db import engine
from database.models import Guild
class GuildListener(commands.Cog, name="Guild Listener"):
@@ -18,7 +17,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
async def on_guild_join(self, guild: nextcord.Guild) -> None:
"""
Listener for Guild Join. Adds guild to database.
@@ -26,12 +25,12 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was joined.
"""
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
# just in case check if the guild already exists
if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name)
new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
@@ -45,7 +44,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
await imcompiling.send(f'Agon has joined server "{guild.name}"!')
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
async def on_guild_remove(self, guild: nextcord.Guild) -> None:
"""
Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false.
@@ -53,7 +52,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was left.
"""
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
guild_data.Active = False
session.commit()
@@ -66,6 +65,6 @@ class GuildListener(commands.Cog, name="Guild Listener"):
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(GuildListener(bot))
+3 -5
View File
@@ -1,6 +1,4 @@
"""
Initializing code for all loops.
"""
"""Initializing code for all loops."""
from nextcord.ext import commands
@@ -10,7 +8,7 @@ from .guild_data import GuildData
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(BotPresence(bot))
bot.add_cog(GuildData(bot))
+8 -11
View File
@@ -1,6 +1,4 @@
"""
Functions for controlling the bot's presence. Also contains the loop.
"""
"""Functions for controlling the bot's presence. Also contains the loop."""
import random
@@ -11,27 +9,26 @@ from nextcord.ext import commands, tasks
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument
async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
self.periodic_change_presence.start()
@tasks.loop(minutes=2)
async def periodic_change_presence(self):
async def periodic_change_presence(self) -> None:
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
"Now with slash commands!",
@@ -41,7 +38,7 @@ class BotPresence(commands.Cog, name="Bot Presence"):
"That's a lot of damage!",
]
status = random.choice(statuses)
status = random.choice(statuses) # noqa: S311
status += f" | {len(self.bot.guilds)} servers"
+12 -16
View File
@@ -1,46 +1,42 @@
"""
Contains a loop that updates guild data in the database periodically.
"""
"""Contains a loop that updates guild data in the database periodically."""
from nextcord.ext import commands, tasks
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
from database.db import engine
from database.models import Guild
class GuildData(commands.Cog, name="Guild Data"):
"""Class to contain loop."""
def __init__(self, bot: commands.bot) -> None:
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_update_guild_data is subclassed from tasks.loop.
await self.periodic_update_guild_data.start() # pylint: disable=no-member
await self.periodic_update_guild_data.start()
@tasks.loop(hours=24)
async def periodic_update_guild_data(self):
async def periodic_update_guild_data(self) -> None:
"""Loops every day, calls update_guild_data."""
self.update_guild_data()
def update_guild_data(self):
"""
Updates all known guild's data in the database.
"""
def update_guild_data(self) -> None:
"""Updates all known guild's data in the database."""
with Session(engine) as session:
for guild in self.bot.guilds:
# update given guild name for guild id
stmt = select(Guild).where(Guild.GuildId == guild.id)
stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
# just in case checking if the guild doesn't exist.
if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name)
new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
+5 -7
View File
@@ -1,6 +1,4 @@
"""
Cog for creating submission process.
"""
"""Cog for creating submission process."""
from nextcord.ext import commands
@@ -15,14 +13,14 @@ class SubmissionCog(commands.Cog, name="Submission Cog"):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Called when cog is loaded"""
async def on_ready(self) -> None:
"""Called when cog is loaded."""
self.bot.add_view(SubmissionCreateButton(self.bot))
self.bot.add_view(VerificationView())
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(SubmissionCog(bot))
+8 -9
View File
@@ -3,10 +3,10 @@
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from sqlmodel import col
from database_setup.sql_data_classes import Submission, engine
# pylint: disable=import-error
from database.db import engine
from database.models import Submission
from utilities.utils import submit_score
@@ -22,8 +22,8 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session:
stmt = (
delete(Submission)
.where(Submission.GuildId == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
session.execute(stmt)
session.commit()
@@ -35,8 +35,8 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session:
stmt = (
select(Submission)
.where(Submission.GuildId == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
submission = session.scalar(stmt)
@@ -47,11 +47,10 @@ class VerificationView(nextcord.ui.View):
return
await submit_score(
interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score
interaction, submission.group_id, submission.category_id, submission.username, submission.score
)
session.delete(submission)
session.commit()
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
# remove the bot information embed. it should always be the first one.
await interaction.message.edit(content=content, view=None)
+1
View File
@@ -0,0 +1 @@
"""Package for database interaction."""
+40
View File
@@ -0,0 +1,40 @@
import os
from dotenv import load_dotenv
from sqlmodel import Session, SQLModel, create_engine, select
from models import CategoryGroup, Guild
load_dotenv()
SQL_DB = os.getenv("SQL_DB", "")
SQL_HOST = os.getenv("SQL_HOST", "")
SQL_PASS = os.getenv("SQL_PASS", "")
SQL_USER = os.getenv("SQL_USER", "")
url = "sqlite:///database.db"
# url = f"mysql+pymysql://{SQL_USER}:{SQL_PASS}@{SQL_HOST}/{SQL_DB}"
engine = create_engine(url)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
guild = Guild(id=1, name="test guild")
session.add(guild)
group1 = CategoryGroup(name="group1", channel_id=1, guild_id=1, message_id=1)
group2 = CategoryGroup(name="group2", channel_id=2, guild_id=1, message_id=2)
session.add(group1)
session.add(group2)
session.commit()
# anotherone = Guild(id=2, name="test guild 2", submission_channel=1, verification_channel=2)
# session.add(anotherone)
# group1 = CategoryGroup(name="group1", channel_id=1, guild_id=2, message_id=1)
# group2 = CategoryGroup(name="group2", channel_id=2, guild_id=2, message_id=2)
# session.add(group1)
# session.add(group2)
# session.commit()
stmt = select(Guild).where(Guild.name == "Dakiny")
guild = session.scalar(stmt)
print(guild.category_groups)
+15
View File
@@ -0,0 +1,15 @@
"""Simple file to set up the sqlmodel connection to database."""
from sqlmodel import SQLModel, create_engine
from utilities import config
from . import models # noqa: F401 - Need this import for creating the models.
if config.ENVIRONMENT == "prod":
url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
else:
url = "sqlite:///database.db"
engine = create_engine(url)
# Don't need to create all when connected to prod.
SQLModel.metadata.create_all(engine)
+186
View File
@@ -0,0 +1,186 @@
"""File holds all model definitions of SQL tables."""
from sqlalchemy.dialects.mysql import BIGINT, BOOLEAN, FLOAT, INTEGER, SMALLINT, VARCHAR
from sqlmodel import Column, Field, ForeignKey, Relationship, SQLModel, UniqueConstraint
class Guild(SQLModel, table=True):
"""
Represents a Guild entity in the database.
Attributes:
id (int): The unique identifier for the guild.
name (str): The name of the guild.
submission_channel (int): The ID of the submission channel associated with the guild.
verification_channel (int): The ID of the verification channel associated with the guild.
active (bool): Indicates whether the guild is active. Defaults to True.
category_groups (list[CategoryGroup]): A list of category groups associated with the guild.
"""
__tablename__: str = "Guilds"
id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), primary_key=True, nullable=False))
name: str = Field(sa_column=Column("GuildName", VARCHAR), max_length=95)
# deprecated, not currently used.
submission_channel: int | None = Field(
sa_column=Column("SubmissionChannel", BIGINT(unsigned=True), nullable=True), default=None
)
verification_channel: int | None = Field(
sa_column=Column("VerificationChannel", BIGINT(unsigned=True), nullable=True), default=None
)
active: bool = Field(sa_column=Column("Active", BOOLEAN), default=True)
category_groups: list["CategoryGroup"] = Relationship(back_populates="guild", cascade_delete=True)
class CategoryGroup(SQLModel, table=True):
"""
Represents a category group in the database.
Attributes:
id (int | None): The primary key of the category group.
name (str): The name of the category group, with a maximum length of 50 characters.
channel_id (int): The ID of the channel associated with the category group.
guild_id (int): The ID of the guild associated with the category group.
message_id (int): The ID of the message associated with the category group.
pad_length (int): The padding length for the category group, default is 12.
username_length (int): The username length for the category group, default is 12.
guild (Guild | None): The guild relationship back-populated from the Guild model.
Table Constraints:
__table_args__: Unique constraint on guild_id and name to ensure unique category groups within a guild.
"""
__tablename__: str = "CategoryGroups"
id: int | None = Field(sa_column=Column("GroupId", INTEGER, primary_key=True), default=None)
name: str = Field(sa_column=Column("GroupName", VARCHAR), max_length=50)
channel_id: int = Field(sa_column=Column("ChannelId", BIGINT(unsigned=True)))
guild_id: int = Field(
sa_column=Column(
"GuildId",
BIGINT(unsigned=True),
ForeignKey("Guilds.GuildId", ondelete="CASCADE"),
nullable=False,
)
)
message_id: int = Field(sa_column=Column("MessageId", BIGINT(unsigned=True)))
pad_length: int = Field(default=12, sa_column=Column("PadLength", SMALLINT(unsigned=True)))
username_length: int = Field(default=12, sa_column=Column("UsernameLength", SMALLINT(unsigned=True)))
__table_args__ = (UniqueConstraint("GuildId", "GroupName", name="UniqueGuildCategoryGroup"),)
guild: Guild | None = Relationship(back_populates="category_groups")
categories: list["Category"] | None = Relationship(back_populates="category_group", cascade_delete=True)
class Category(SQLModel, table=True):
"""
Represents a category in the database.
Attributes:
id (int): The unique identifier for the category. Primary key.
name (str): The name of the category with a maximum length of 30 characters.
group_id (int): The identifier of the group to which the category belongs.
Foreign key referencing CategoryGroups.id.
size (int): The size attribute of the category, default is 3.
is_ascending (bool): Indicates if the category is in ascending order. Default is True.
is_time_record (bool): Indicates if the category is a time record. Default is False.
is_validated (bool): Indicates if the category is validated. Default is True.
category_group (CategoryGroup | None): The relationship to the CategoryGroup model.
scores (list["Score"] | None): The relationship to the Score model with cascade delete enabled.
Table Constraints:
__table_args__: Unique constraint on group_id and name to ensure unique category names within a group.
"""
__tablename__: str = "Categories"
id: int = Field(sa_column=Column("CategoryId", INTEGER, primary_key=True), default=None)
name: str = Field(sa_column=Column("CategoryName", VARCHAR), max_length=30)
group_id: int = Field(
sa_column=Column(
"GroupId",
INTEGER(unsigned=True),
ForeignKey("CategoryGroups.GroupId", ondelete="CASCADE"),
nullable=False,
)
)
size: int = Field(sa_column=Column("Size", SMALLINT(unsigned=True), nullable=False), default=3)
is_ascending: bool = Field(sa_column=Column("IsAscending", BOOLEAN, nullable=False), default=True)
is_time_record: bool = Field(sa_column=Column("IsTimeRecord", BOOLEAN, nullable=False), default=False)
is_validated: bool = Field(sa_column=Column("IsValidated", BOOLEAN, nullable=False), default=True)
__table_args__ = (UniqueConstraint("GroupId", "CategoryName", name="UniqueCategoryInGroup"),)
category_group: CategoryGroup | None = Relationship(back_populates="categories")
scores: list["Score"] | None = Relationship(back_populates="category", cascade_delete=True)
class Score(SQLModel, table=True):
"""
Represents a score entry in the database.
Attributes:
username (str): The username associated with the score. This is a primary key.
score (float): The score value.
category_id (int): The ID of the category this score belongs to. This is a primary key and a foreign key
referencing the Categories table.
category (Category | None): The relationship to the Category model, back-populated by the scores attribute
in the Category model.
"""
__tablename__: str = "Scores"
username: str = Field(sa_column=Column("UserName", VARCHAR, primary_key=True, nullable=False), max_length=30)
score: float = Field(sa_column=Column("Score", FLOAT, nullable=False))
category_id: int = Field(
sa_column=Column(
"CategoryId",
INTEGER(unsigned=True),
ForeignKey("Categories.CategoryId", ondelete="CASCADE"),
nullable=False,
primary_key=True,
),
)
category: Category | None = Relationship(back_populates="scores")
class Submission(SQLModel, table=True):
"""
Represents a submission in the database.
Attributes:
category_id (int): The ID of the category. This is a primary key.
group_id (int): The ID of the group.
guild_id (int): The ID of the guild.
message_id (int): The ID of the message.
score (str): The score of the submission, with a maximum length of 30 characters.
user_id (int): The ID of the user.
username (str): The username of the user. This is a primary key.
"""
__tablename__: str = "Submissions"
category_id: int = Field(
sa_column=Column(
"CategoryId",
INTEGER(unsigned=True),
nullable=False,
primary_key=True,
),
)
group_id: int = Field(
sa_column=Column(
"GroupId",
INTEGER(unsigned=True),
nullable=False,
)
)
guild_id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), nullable=False))
message_id: int = Field(sa_column=Column("MessageId", BIGINT(unsigned=True), nullable=False))
# Score is stored as a string until converted during submission.
score: str = Field(sa_column=Column("Score", VARCHAR, nullable=False), max_length=30)
user_id: int = Field(sa_column=Column("UserId", BIGINT(unsigned=True), nullable=False))
username: str = Field(sa_column=Column("UserName", VARCHAR, nullable=False, primary_key=True), max_length=30)
-13
View File
@@ -1,13 +0,0 @@
"""Small script to create the database if it doesn't already exist."""
import sqlite3
from pathlib import Path
if not Path("database/dakiny.db").exists():
with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor()
with Path("database_setup/database.sql", encoding="utf-8").open() as file:
database_script = file.read()
cur.executescript(database_script)
con.commit()
-18
View File
@@ -1,18 +0,0 @@
"""Simple file to set up the sqlalchemy connection to database."""
from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
from utilities import config
# connect with sql alchemy and autoload schema
url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
engine = create_engine(url)
Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
Score = Base.classes.Scores
Submission = Base.classes.Submissions
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies = [
"pymysql>=1.1.1",
"python-dotenv>=1.0.1",
"setuptools>=75.3.0",
"sqlalchemy>=2.0.35",
"sqlmodel>=0.0.22",
]
requires-python = ">=3.12"
+1
View File
@@ -0,0 +1 @@
"""Package of utility functions and environment configuration variables."""
+2 -3
View File
@@ -1,6 +1,4 @@
"""
Loads environment variables for use in bot.
"""
"""Loads environment variables for use in bot."""
import os
@@ -15,3 +13,4 @@ SQL_DB = os.getenv("SQL_DB", "")
SQL_HOST = os.getenv("SQL_HOST", "")
SQL_PASS = os.getenv("SQL_PASS", "")
SQL_USER = os.getenv("SQL_USER", "")
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "prod")
+60 -56
View File
@@ -6,9 +6,10 @@ import nextcord
import pandas as pd
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session, aliased
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, Score, engine
from database.db import engine
from database.models import Category, CategoryGroup, Guild, Score
MAX_LINE_LENGTH = 142
@@ -31,20 +32,19 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
# search database for all category names
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == guild_id)
.where(CategoryGroup.GroupId == group_id)
.where(col(CategoryGroup.guild_id) == guild_id)
.where(col(CategoryGroup.id) == group_id)
.subquery()
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
categories_dict: dict[str, str] = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)}
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
categories_dict: dict[str, str] = {cat.name: str(cat.id) for cat in session.scalars(stmt)}
# if string empty, return all category names
if not category_begin:
return dict(list(categories_dict.items())[:25])
# else find all categories that start with category_begin
# TODO is it better to have sql handle the starts with or python?
categories: dict[str, str] = {
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
}
@@ -64,10 +64,10 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
"""
with Session(engine) as session:
# Select all groups in guild.
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id)
stmt = select(CategoryGroup).wherecol((CategoryGroup.guild_id) == guild_id)
# dict has to have key values of same type for discord.
group_dict: dict[str, str] = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
group_dict: dict[str, str] = {group.name: str(group.id) for group in session.scalars(stmt)}
# if string empty, return all group names
if not group_begin:
@@ -83,8 +83,10 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
"""
Returns list of users that match user_begin. Either all users in group or just users in category if category is
not None. Might be expensive due to unioning all users in group.
Returns list of users that match user_begin.
Either all users in group or just users in category if category is not None. Might be expensive due to union-ing all
users in group.
Args:
guild_id (int): Guild id corresponding to group and category.
@@ -99,27 +101,27 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
# get all categories in the group
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == guild_id)
.where(CategoryGroup.GroupId == group_id)
.where(col(CategoryGroup.guild_id) == guild_id)
.where(col(CategoryGroup.id) == group_id)
.subquery()
)
category_group = aliased(CategoryGroup, subq)
# If no category id, get all users in group.
if category_id == 0:
subq = select(Category.CategoryId).where(Category.GroupId == category_group.GroupId)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
subq = select(col(Category.id)).where(col(Category.group_id) == category_group.id)
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(subq))
else:
# get all users in category_id
subq = (
select(Category)
.where(Category.GroupId == category_group.GroupId)
.where(Category.CategoryId == category_id)
.where(col(Category.group_id) == category_group.id)
.where(col(Category.id) == category_id)
.subquery()
)
category = aliased(Category, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId)
stmt = select(distinct(col(Score.username))).where(col(Score.category_id) == category.id)
users_list: list[str] = list(session.scalars(stmt))
@@ -145,9 +147,9 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
list[str]: All users with submissions in the guild.
"""
with Session(engine) as session:
group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id)
category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids))
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids))
group_ids = select(col(CategoryGroup.id)).where(col(CategoryGroup.guild_id) == guild_id)
category_ids = select(col(Category.id)).where(col(Category.group_id).in_(group_ids))
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(category_ids))
users_list: list[str] = list(session.scalars(stmt))
# if string empty, return all users
@@ -176,34 +178,34 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
Returns:
str: Formatted string for discord message
"""
ret_string = f"**{group.GroupName}**\n```"
ret_string = f"**{group.name}**\n```"
stmt = select(Guild).where(Guild.GuildId == group.GuildId)
stmt = select(Guild).where(col(Guild.id) == group.guild_id)
# Get all categories in the group
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == group.GuildId)
.where(CategoryGroup.GroupName == group.GroupName)
.where(col(CategoryGroup.guild_id) == group.guild_id)
.where(col(CategoryGroup.name) == group.name)
.subquery()
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
# set up a map for the categories
# also grab the max category size
categories = []
categories: list[Category] = []
category_ids = []
max_cat_size: int = 0
pad_length = group.PadLength
pad_length = group.pad_length
for cat in session.scalars(stmt):
max_cat_size = max(cat.Size, max_cat_size)
max_cat_size = max(cat.size, max_cat_size)
categories.append(cat)
category_ids.append(cat.CategoryId)
category_ids.append(cat.id)
# get all scores in category group, load into data frame.
d_f = pd.read_sql_query(
sql=session.query(Score).where(Score.CategoryId.in_(category_ids)).statement,
sql=session.query(Score).where(col(Score.category_id).in_(category_ids)).statement,
con=engine,
)
@@ -214,15 +216,15 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
for category in categories:
# category name string
str_list[0] += f"{category.CategoryName}".ljust(pad_length, " ")
str_list[0] += f"{category.name}".ljust(pad_length, " ")
# ensure there is data to print
if category.CategoryId in groups_df.indices:
group_df = groups_df.get_group(category.CategoryId)
if category.id in groups_df.indices:
group_df = groups_df.get_group(category.id)
# process data. Want each group sorted correctly and only top X results
processed_group = (
group_df.sort_values("Score", ascending=category.IsAscending).head(category.Size).reset_index()
group_df.sort_values("Score", ascending=category.is_ascending).head(category.size).reset_index()
)
# add each score to list
@@ -234,19 +236,19 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
str_list[ind + 1] += format_score_string(
username,
score,
group.UsernameLength,
category.IsTimeRecord,
group.username_length,
category.is_time_record,
ind + 1,
pad_length,
)
# if not enough entries, pad out
elif ind + 1 <= category.Size:
elif ind + 1 <= category.size:
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
else:
str_list[ind + 1] += "".ljust(pad_length, " ")
else:
for ind in range(max_cat_size):
if ind + 1 <= category.Size:
if ind + 1 <= category.size:
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
else:
str_list[ind + 1] += "".ljust(pad_length, " ")
@@ -301,13 +303,13 @@ def format_score_string(
if is_time_record:
# MM:SS.ms is 7 characters long
# if there are single digit minutes, add a space for formatting.
score = get_time_str(score)
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
formatted_score = get_time_str(score)
if formatted_score.find(":") and len(formatted_score.split(":", maxsplit=1)[0]) < 2: # noqa: PLR2004
formatted_score = " " + formatted_score
else:
score = str(int(score)).rjust(7, " ")
formatted_score = str(int(score)).rjust(7, " ")
return f"{rank}: {username} - {score}".ljust(pad_length, " ")
return f"{rank}: {username} - {formatted_score}".ljust(pad_length, " ")
def get_time_str(time_in: float) -> str:
@@ -328,6 +330,7 @@ def get_time_str(time_in: float) -> str:
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
"""
Function takes care of checking if a message exists for a group.
Creates and sends/edits the message.
Args:
@@ -339,15 +342,15 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
nextcord.Message: Message object that was posted with group contents.
"""
highscore_string = format_highscore_message(session, group)
channel = guild.get_channel(group.ChannelId)
channel = guild.get_channel(group.channel_id)
if group.MessageId is None:
if group.message_id is None:
# message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string)
group.MessageId = message.id
session.commit()
else:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.edit(content=highscore_string)
return message
@@ -362,37 +365,38 @@ async def submit_score(
) -> None:
"""
Adds passed score to group. Then updates group message by calling send_highscore_message.
Ensures that a highscores channel exists.
Args:
interaction (nextcord.Interaction): Interaction that caused the score submission.
group (CategoryGroup): group to submit score to.
category_id (Category): Category to submit score to.
group_id (int): group id to submit score to.
category_id (int): Category id to submit score to.
username (str): The user who submitted the score.
score (str): Score to submit. Need to convert to correct type.
"""
with Session(engine) as session:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == category_id)
stmt = select(col(Category.is_time_record)).where(col(Category.id) == category_id)
is_time_record = session.scalar(stmt)
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
score = (minutes * 60) + seconds
score_f = (minutes * 60) + seconds
else:
score = float(score)
score_f = float(score)
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id)
stmt = select(Score).where(col(Score.username) == username).where(col(Score.category_id) == category_id)
existing_score = session.scalar(stmt)
if existing_score:
existing_score.Score = score
existing_score.Score = score_f
else:
score_obj = Score(UserName=username, Score=score, CategoryId=category_id)
score_obj = Score(username=username, score=score_f, category_id=category_id)
session.add(score_obj)
session.commit()
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == group_id)
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
group = session.scalar(stmt)
await send_highscore_message(session, interaction.guild, group)
Generated
+77 -2
View File
@@ -12,7 +12,7 @@ dependencies = [
{ name = "pymysql" },
{ name = "python-dotenv" },
{ name = "setuptools" },
{ name = "sqlalchemy" },
{ name = "sqlmodel" },
]
[package.dev-dependencies]
@@ -28,7 +28,7 @@ requires-dist = [
{ name = "pymysql", specifier = ">=1.1.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "setuptools", specifier = ">=75.3.0" },
{ name = "sqlalchemy", specifier = ">=2.0.35" },
{ name = "sqlmodel", specifier = ">=0.0.22" },
]
[package.metadata.requires-dev]
@@ -102,6 +102,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "attrs"
version = "24.2.0"
@@ -425,6 +434,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pymysql"
version = "1.1.1"
@@ -536,6 +598,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
]
[[package]]
name = "sqlmodel"
version = "0.0.22"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"