Compare commits

...
Author SHA1 Message Date
imcompiling 00212ea09d Update dockerfile to use poetry. 2024-10-05 12:23:30 -04:00
imcompiling 96c902fcc3 Rename highscores.py to main.py 2024-10-05 12:23:16 -04:00
imcompiling c177ae67d6 Remove requirements. 2024-10-05 12:22:57 -04:00
imcompiling b73cddab53 Add ruff to project. 2024-10-05 11:52:28 -04:00
imcompiling 8f88c76e2e Add pyproject for poetry support. 2024-10-05 11:47:12 -04:00
imcompiling c977a4e958 Bug fix #64, was using wrong variable.
build_develop / build (push) Successful in 46s
Utils.py got formatted by ruff.
2024-09-30 12:55:34 -04:00
imcompiling e2e8d8de30 Don't need to print the help string.
build_develop / build (push) Successful in 1m20s
build_release / build (push) Successful in 20s
2024-09-29 23:31:49 -04:00
imcompiling 7f007ffa1f Merge commit '02e7a6df3a13bb19056f31f616101980b668ae43'
build_develop / build (push) Successful in 2m20s
2023-08-30 09:04:36 -04:00
imcompiling 913f915bf0 Clarify create category option description. 2023-08-30 09:04:24 -04:00
10 changed files with 218 additions and 43 deletions
+1
View File
@@ -105,6 +105,7 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
poetry.lock
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
+5 -7
View File
@@ -1,21 +1,19 @@
FROM python:3.8.10-slim FROM python:3.12.3-slim
# install dependencies for mysqlclient # install dependencies for mysqlclient
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential # RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
RUN pip install --upgrade pip
WORKDIR /home WORKDIR /home
COPY classes classes/ COPY classes classes/
COPY cogs cogs/ COPY cogs cogs/
COPY utilities utilities/ COPY utilities utilities/
COPY highscores.py highscores.py COPY main.py main.py
COPY requirements.txt requirements.txt COPY pyproject.toml pyproject.toml
RUN pip install -r requirements.txt RUN pip install --upgrade pip && pip install poetry && poetry export -f requirements.txt --output requirements.txt && pip install -r requirements.txt
# remove build deps that aren't needed to run # remove build deps that aren't needed to run
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove # RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "./highscores.py"] CMD ["python3", "./main.py"]
+1 -1
View File
@@ -40,7 +40,7 @@ class CreateCog(commands.Cog, name="Create Cog"):
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True), group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True), category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If higher value is better score.", required=True), is_ascending: bool = nextcord.SlashOption(description="If lower value is a better score.", required=True),
is_time_record: bool = nextcord.SlashOption( is_time_record: bool = nextcord.SlashOption(
description="If the score should be shown in time format. Cannot be changed.", required=True description="If the score should be shown in time format. Cannot be changed.", required=True
), ),
-1
View File
@@ -42,6 +42,5 @@ This channel will be where all user submission messages are sent.
3.) Set up your highscore tables! Use "/create group" to first create a group message in the channel you would like and then use "/create category" to create a category in that group message!""" 3.) Set up your highscore tables! Use "/create group" to first create a group message in the channel you would like and then use "/create category" to create a category in that group message!"""
# pylint: enable=line-too-long # pylint: enable=line-too-long
print(help_str)
await interaction.response.send_message(content=help_str) await interaction.response.send_message(content=help_str)
+1 -1
View File
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from classes.submission import SubmissionState from classes.submission import SubmissionState
from highscores import submission_objects from main import submission_objects
from utilities.sql_data_classes import Category, Guild, engine from utilities.sql_data_classes import Category, Guild, engine
+2 -4
View File
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error # pylint: disable=import-error
from classes.submission import Submission, SubmissionState from classes.submission import Submission, SubmissionState
from highscores import submission_objects from main import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine
from .verification_view import VerificationView from .verification_view import VerificationView
@@ -147,9 +147,7 @@ class SubmissionCreateButton(nextcord.ui.View):
self._bot = bot self._bot = bot
@nextcord.ui.button(label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate") @nextcord.ui.button(label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate")
async def submitcreate_button( async def submitcreate_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): # pylint: disable=unused-argument
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for score submission.""" """Button for score submission."""
# Don't allow a user to start a new submission if one already exists # Don't allow a user to start a new submission if one already exists
View File
+130
View File
@@ -0,0 +1,130 @@
[tool.poetry]
name = "agon"
version = "0.1.0"
description = "A highscores discord bot"
authors = ["imcompiling <[email protected]>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
nextcord = ">=2.3.2"
python-dotenv = ">=0.20.0"
pandas = ">=1.5.2"
sqlalchemy = "<2.0"
pymysql = ">=1.0.2"
cryptography = ">=2.8"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"dakiny-messages", # Don't need to lint this git repo.
]
# Same as Black.
line-length = 120
indent-width = 4
# Assume Python 3.8
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["ALL"]
ignore = [
"ANN101",
"B008", # I build SlashOptions as arguments.
"COM812",
"COM819",
"D107", # don't want docstrings on init functions
"D203",
"D206",
"D212",
"D300",
"E111",
"E114",
"E117",
"ISC001",
"ISC002",
"PD011", # doesn't like me using '.values' on embeds....
"Q000",
"Q001",
"Q002",
"Q003",
"W191",
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = true
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
-6
View File
@@ -1,6 +0,0 @@
nextcord>=2.3.2
python-dotenv>=0.20.0
pandas>=1.5.2
sqlalchemy<2.0
pymysql>=1.0.2
cryptography>=2.8
+78 -23
View File
@@ -16,7 +16,9 @@ from utilities.sql_data_classes import Category, CategoryGroup, Guild, Score, en
MAX_LINE_LENGTH = 142 MAX_LINE_LENGTH = 142
def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "dict[str, str]": def autofill_categories(
guild_id: int, group_id: int, category_begin: str
) -> "dict[str, str]":
""" """
Returns all category names that start with category_begin. Returns all category names that start with category_begin.
If category_begin is empty, returns list of all category names. If category_begin is empty, returns list of all category names.
@@ -40,16 +42,20 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
) )
cat_group_alias = aliased(CategoryGroup, subq) cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId) 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)} categories_dict: "dict[str, str]" = {
cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)
}
# if string empty, return all category names # if string empty, return all category names
if not category_begin: if not category_begin:
return dict(list(category_begin.items())[:25]) return dict(list(categories_dict.items())[:25])
# else find all categories that start with category_begin # else find all categories that start with category_begin
# TODO is it better to have sql handle the starts with or python? # TODO is it better to have sql handle the starts with or python?
categories: "dict[str, str]" = { 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() cat_name: cat_id
for cat_name, cat_id in categories_dict.items()
if category_begin.lower() in cat_name.lower()
} }
return dict(list(categories.items())[:25]) return dict(list(categories.items())[:25])
@@ -71,7 +77,9 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id) stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id)
# dict has to have key values of same type for discord. # 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.GroupName: str(group.GroupId) for group in session.scalars(stmt)
}
# if string empty, return all group names # if string empty, return all group names
if not group_begin: if not group_begin:
@@ -79,13 +87,17 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
# else find all groups that start with group_begin # else find all groups that start with group_begin
groups: "dict[str, str]" = { groups: "dict[str, str]" = {
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower() group_name: group_id
for group_name, group_id in group_dict.items()
if group_begin.lower() in group_name.lower()
} }
return dict(list(groups.items())[:25]) return dict(list(groups.items())[:25])
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[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 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. not None. Might be expensive due to unioning all users in group.
@@ -112,7 +124,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
# If no category id, get all users in group. # If no category id, get all users in group.
if category_id == 0: if category_id == 0:
subq = select(Category.CategoryId).where(Category.GroupId == category_group.GroupId) subq = select(Category.CategoryId).where(
Category.GroupId == category_group.GroupId
)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq)) stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
else: else:
@@ -124,7 +138,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
.subquery() .subquery()
) )
category = aliased(Category, subq) category = aliased(Category, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId) stmt = select(distinct(Score.UserName)).where(
Score.CategoryId == category.CategoryId
)
users_list: "list[str]" = list(session.scalars(stmt)) users_list: "list[str]" = list(session.scalars(stmt))
@@ -133,7 +149,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
return users_list[:25] return users_list[:25]
# else find all users that start with user_begin # else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()] users: "list[str]" = [
user for user in users_list if user_begin.lower() in user.lower()
]
return users[:25] return users[:25]
@@ -151,9 +169,15 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
""" """
with Session(engine) as session: with Session(engine) as session:
group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id) group_ids = select(CategoryGroup.GroupId).where(
category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids)) CategoryGroup.GuildId == guild_id
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids)) )
category_ids = select(Category.CategoryId).where(
Category.GroupId.in_(group_ids)
)
stmt = select(distinct(Score.UserName)).where(
Score.CategoryId.in_(category_ids)
)
users_list: "list[str]" = list(session.scalars(stmt)) users_list: "list[str]" = list(session.scalars(stmt))
# if string empty, return all users # if string empty, return all users
@@ -161,7 +185,9 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
return users_list[:25] return users_list[:25]
# else find all users that start with user_begin # else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()] users: "list[str]" = [
user for user in users_list if user_begin.lower() in user.lower()
]
return users[:25] return users[:25]
@@ -207,7 +233,10 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
category_ids.append(cat.CategoryId) category_ids.append(cat.CategoryId)
# get all scores in category group, load into data frame. # 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, con=engine) d_f = pd.read_sql_query(
sql=session.query(Score).where(Score.CategoryId.in_(category_ids)).statement,
con=engine,
)
# this list is what will be printed in the discord message # this list is what will be printed in the discord message
# first string will be category list, others will be score printouts # first string will be category list, others will be score printouts
@@ -225,17 +254,26 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
# process data. Want each group sorted correctly and only top X results # process data. Want each group sorted correctly and only top X results
processed_group = ( processed_group = (
group_df.sort_values("Score", ascending=category.IsAscending).head(category.Size).reset_index() group_df.sort_values("Score", ascending=category.IsAscending)
.head(category.Size)
.reset_index()
) )
# add each score to list # add each score to list
for ind in range(max_cat_size): # need to subtract to deal with category name string for ind in range(
max_cat_size
): # need to subtract to deal with category name string
if ind in processed_group.index: if ind in processed_group.index:
username = processed_group["UserName"][ind] username = processed_group["UserName"][ind]
score = processed_group["Score"][ind] score = processed_group["Score"][ind]
str_list[ind + 1] += format_score_string( str_list[ind + 1] += format_score_string(
username, score, group.UsernameLength, category.IsTimeRecord, ind + 1, pad_length username,
score,
group.UsernameLength,
category.IsTimeRecord,
ind + 1,
pad_length,
) )
else: else:
# if not enough entries, pad out # if not enough entries, pad out
@@ -275,7 +313,12 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
def format_score_string( def format_score_string(
username: str, score: float, username_length: int, is_time_record: bool, rank: int, pad_length: int username: str,
score: float,
username_length: int,
is_time_record: bool,
rank: int,
pad_length: int,
) -> str: ) -> str:
""" """
Builds the string to show a user's score. Builds the string to show a user's score.
@@ -321,7 +364,9 @@ def get_time_str(time_in: float) -> str:
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in)) return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message: 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. Function takes care of checking if a message exists for a group.
Creates and sends/edits the message. Creates and sends/edits the message.
@@ -351,7 +396,11 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
async def submit_score( async def submit_score(
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: str interaction: nextcord.Interaction,
group_id: int,
category_id: int,
username: str,
score: str,
) -> None: ) -> None:
""" """
Adds passed score to group. Then updates group message by calling send_highscore_message. Adds passed score to group. Then updates group message by calling send_highscore_message.
@@ -376,7 +425,11 @@ async def submit_score(
else: else:
score = float(score) score = float(score)
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id) stmt = (
select(Score)
.where(Score.UserName == username)
.where(Score.CategoryId == category_id)
)
existing_score = session.scalar(stmt) existing_score = session.scalar(stmt)
if existing_score: if existing_score:
@@ -403,7 +456,9 @@ def add_preset_configuration(guild_id: int, config_str: str) -> None:
""" """
with Session(engine) as session: with Session(engine) as session:
with open(f"example_config/{config_str}.json", encoding="utf-8", mode="r") as file: with open(
f"example_config/{config_str}.json", encoding="utf-8", mode="r"
) as file:
config = json.load(file) config = json.load(file)
for group_name, categorys in config["tables"].items(): for group_name, categorys in config["tables"].items():