diff --git a/.env.example b/.env.example index 5575e7b..61a8672 100644 --- a/.env.example +++ b/.env.example @@ -16,10 +16,12 @@ BOT_MISC_COOLDOWN=20 BOT_OPENAI_COOLDOWN=10 BOT_OWNER_COOLDOWN=5 + # OpenAI API key BOT_OPENAI_MODEL=gpt-5.2 OPENAI_API_KEY= + # ddcDatabases configs POSTGRESQL_HOST=postgres POSTGRESQL_PORT=5432 @@ -55,6 +57,7 @@ POSTGRESQL_OP_INITIAL_RETRY_DELAY=0.5 POSTGRESQL_OP_MAX_RETRY_DELAY=10.0 POSTGRESQL_OP_JITTER=0.1 + # pythonLogs configs LOG_LEVEL=INFO LOG_TIMEZONE=UTC @@ -72,6 +75,7 @@ LOG_LOGGER_TTL_SECONDS=1800 LOG_ROTATE_WHEN=midnight LOG_ROTATE_AT_UTC=True + # GW2 configuration GW2_API_VERSION=2 GW2_EMBED_COLOR=green diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 431cf26..eba1f84 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -15,13 +15,7 @@ - [ ] Other ## Testing -- [ ] Unit tests added/updated -- [ ] Integration tests added/updated - [ ] Manual testing performed -## Checklist -- [ ] Documentation updated (if applicable) -- [ ] I have considered how this change may affect other services - ## Reviewer - [ ] I understand that by approving this PR, I share responsibility for these changes diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index f573770..96d9b96 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -81,6 +81,7 @@ jobs: docker: name: Docker runs-on: ubuntu-latest + needs: lint steps: - uses: actions/checkout@v6 @@ -109,7 +110,7 @@ jobs: pages: name: Deploy Pages runs-on: ubuntu-latest - needs: [test, integration-test] + needs: [test, integration-test, docker] if: startsWith(github.ref, 'refs/tags/v') permissions: pages: write @@ -148,7 +149,7 @@ jobs: release: name: Create Release runs-on: ubuntu-latest - needs: [test, integration-test] + needs: [test, integration-test, docker] if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write @@ -157,7 +158,4 @@ jobs: uses: softprops/action-gh-release@v2 with: name: Release ${{ github.ref_name }} - body: | - Automated release for version ${{ github.ref_name }} - draft: false - prerelease: false + generate_release_notes: true diff --git a/.gitignore b/.gitignore index b8f5974..0c39f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,4 @@ cython_debug/ # Local logs /logs/* +/requirements.txt diff --git a/.snyk b/.snyk new file mode 100644 index 0000000..5785228 --- /dev/null +++ b/.snyk @@ -0,0 +1,20 @@ +# Snyk policy file +# https://docs.snyk.io/scan-using-snyk/snyk-code/configure-snyk-code#excluding-directories-and-files-from-the-snyk-code-test +version: v1.25.0 +exclude: + global: + - tests/** +ignore: + 'snyk:lic:pip:psycopg:LGPL-3.0': + - '*': + reason: psycopg is used as a dependency, not modified - LGPL-3.0 is compatible with MIT + expires: 2027-03-20T00:00:00.000Z + 'snyk:lic:pip:psycopg-binary:LGPL-3.0': + - '*': + reason: psycopg-binary is used as a dependency, not modified - LGPL-3.0 is compatible with MIT + expires: 2027-03-20T00:00:00.000Z + 'snyk:lic:pip:certifi:MPL-2.0': + - '*': + reason: certifi is used as a dependency, not modified - MPL-2.0 is compatible with MIT + expires: 2027-03-20T00:00:00.000Z + diff --git a/Dockerfile b/Dockerfile index cba2f17..047e996 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,9 +21,10 @@ ENV LOG_DIRECTORY="${LOG_DIRECTORY}" \ WORKDIR ${WORKDIR} RUN set -ex && \ - apk upgrade --no-cache zlib && \ + apk update && \ + apk upgrade --no-cache && \ apk add --no-cache ca-certificates curl && \ - curl --proto '=https' -LsSf https://astral.sh/uv/install.sh | sh && \ + curl --proto '=https' --tlsv1.3 -LsSf https://astral.sh/uv/install.sh | sh && \ addgroup -g 1000 botuser && \ adduser -u 1000 -G botuser -h /home/botuser -D botuser && \ mv /root/.local /home/botuser/.local && \ diff --git a/README.md b/README.md index 935951d..2c8ea6b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
-
+
@@ -312,8 +312,8 @@ Released under the [MIT License](LICENSE)
# Support
-If you find this project helpful, consider supporting development:
+If you find this project helpful, consider supporting development.
-- [GitHub Sponsor](https://github.com/sponsors/ddc)
-- [ko-fi](https://ko-fi.com/ddcsta)
-- [PayPal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
+
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 643cea3..b355ec4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "DiscordBot"
-version = "3.0.8"
+version = "3.0.9"
description = "A simple Discord bot with OpenAI support and server administration tools"
urls.Repository = "https://github.com/ddc/DiscordBot"
urls.Homepage = "https://ddc.github.io/DiscordBot"
@@ -33,26 +33,29 @@ dependencies = [
"alembic>=1.18.4",
"beautifulsoup4>=4.14.3",
"better-profanity>=0.7.0",
- "ddcdatabases[postgres]>=3.0.11",
+ "ddcdatabases[postgres]>=4.0.1",
"discord-py>=2.7.1",
"gTTS>=2.5.4",
- "openai>=2.28.0",
+ "openai>=2.29.0",
"PyNaCl>=1.6.2",
- "pythonLogs>=6.0.3",
+ "pythonLogs>=7.0.0",
"uuid-utils>=0.14.1",
]
[dependency-groups]
dev = [
- "coverage>=7.13.4",
+ "coverage>=7.13.5",
"poethepoet>=0.42.1",
"pytest-asyncio>=1.3.0",
- "ruff>=0.15.6",
- "testcontainers[postgres]>=4.14.1",
+ "ruff>=0.15.7",
+ "testcontainers[postgres]>=4.14.2",
]
[tool.poe.tasks]
linter.shell = "uv run ruff check --fix . && uv run ruff format ."
+snyk-export.shell = "rm -f requirements.txt && uv export --no-hashes --no-annotate --format requirements-txt > requirements.txt && uvx pre-commit run --all-files || uvx pre-commit run --all-files"
+snyk-container.shell = "docker build -t discordbot:snyk-scan . && snyk container test discordbot:snyk-scan --file=Dockerfile; docker rmi discordbot:snyk-scan"
+snyk.sequence = ["snyk-export", { shell = "uv pip install pip && snyk test --file=requirements.txt && snyk code test; uv pip uninstall pip" }, "snyk-container"]
profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit"
profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration"
test.sequence = [{ shell = "uv run coverage run -m pytest tests/unit" }, { shell = "uv run coverage report" }, { shell = "uv run coverage xml" }]
@@ -100,15 +103,14 @@ line-length = 120
target-version = "py314"
[tool.ruff.lint]
-select = ["E", "W", "F", "I", "B", "C4", "UP"]
+select = ["E", "W", "F", "I", "B", "C4", "UP", "S", "SLF"]
ignore = ["E501", "E402", "UP046", "UP047"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
-"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001", "F841"]
+"tests/**/*.py" = ["S101", "S105", "S106", "S110", "S311", "S603", "S607", "SLF001", "F841"]
[tool.ruff.lint.isort]
known-first-party = ["DiscordBot"]
-force-sort-within-sections = false
-from-first = false
+no-lines-before = ["future", "standard-library", "third-party", "first-party", "local-folder"]
no-sections = true
diff --git a/src/__main__.py b/src/__main__.py
index 6dc25d9..3d231e3 100644
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -5,8 +5,8 @@
import time
import traceback
from aiohttp import ClientSession
-from ddcDatabases import PostgreSQL
-from pythonLogs import TimedRotatingLog
+from ddcdatabases import PostgreSQL
+from pythonlogs import TimedRotatingLog
from src.bot.constants import messages, variables
from src.bot.constants.settings import get_bot_settings
from src.bot.discord_bot import Bot
diff --git a/src/bot/constants/messages.py b/src/bot/constants/messages.py
index 107a82f..fddcfa7 100644
--- a/src/bot/constants/messages.py
+++ b/src/bot/constants/messages.py
@@ -2,7 +2,7 @@
class Bot:
- TOKEN_NOT_FOUND = "BOT_TOKEN variable not found"
+ MISSING_ENV_VAR = "BOT_TOKEN variable not found"
TERMINATED = "Bot has been terminated."
STOPPED_CTRTC = "Bot stopped with Ctrl+C"
FATAL_ERROR_MAIN = "Fatal error in main()"
@@ -225,7 +225,7 @@ class Owner:
# ============================================================================
# Bot
-BOT_TOKEN_NOT_FOUND = Bot.TOKEN_NOT_FOUND
+BOT_TOKEN_NOT_FOUND = Bot.MISSING_ENV_VAR
BOT_TERMINATED = Bot.TERMINATED
BOT_STOPPED_CTRTC = Bot.STOPPED_CTRTC
BOT_FATAL_ERROR_MAIN = Bot.FATAL_ERROR_MAIN
diff --git a/src/bot/constants/variables.py b/src/bot/constants/variables.py
index 6b90c27..2eff57e 100644
--- a/src/bot/constants/variables.py
+++ b/src/bot/constants/variables.py
@@ -2,7 +2,7 @@
import sys
import tomllib
from pathlib import Path
-from pythonLogs import get_log_settings
+from pythonlogs import get_log_settings
from src.bot.constants.settings import get_bot_settings
from typing import Final
diff --git a/src/bot/tools/custom_help.py b/src/bot/tools/custom_help.py
index b44566c..8e27a43 100644
--- a/src/bot/tools/custom_help.py
+++ b/src/bot/tools/custom_help.py
@@ -14,7 +14,7 @@ def __init__(self, pages: list[str], author_id: int):
self.message: discord.Message | None = None
self._update_buttons()
- def _format_page(self) -> str:
+ def format_page(self) -> str:
page_header = f"**Page {self.current_page + 1}/{len(self.pages)}**\n"
return page_header + self.pages[self.current_page]
@@ -31,7 +31,7 @@ async def previous_button(self, interaction: discord.Interaction, button: discor
)
self.current_page -= 1
self._update_buttons()
- await interaction.response.edit_message(content=self._format_page(), view=self)
+ await interaction.response.edit_message(content=self.format_page(), view=self)
@discord.ui.button(label="1/1", style=discord.ButtonStyle.secondary, disabled=True)
async def page_indicator(self, interaction: discord.Interaction, button: discord.ui.Button):
@@ -45,7 +45,7 @@ async def next_button(self, interaction: discord.Interaction, button: discord.ui
)
self.current_page += 1
self._update_buttons()
- await interaction.response.edit_message(content=self._format_page(), view=self)
+ await interaction.response.edit_message(content=self.format_page(), view=self)
class CustomHelpCommand(commands.DefaultHelpCommand):
@@ -131,7 +131,7 @@ async def _send_pages_to_dm(self):
await self.context.author.send(pages[0])
else:
view = HelpPaginatorView(pages, self.context.author.id)
- msg = await self.context.author.send(content=view._format_page(), view=view)
+ msg = await self.context.author.send(content=view.format_page(), view=view)
view.message = msg
async def _send_pages_to_destination(self, destination):
@@ -141,5 +141,5 @@ async def _send_pages_to_destination(self, destination):
await destination.send(pages[0])
else:
view = HelpPaginatorView(pages, self.context.author.id)
- msg = await destination.send(content=view._format_page(), view=view)
+ msg = await destination.send(content=view.format_page(), view=view)
view.message = msg
diff --git a/src/database/dal/bot/bot_configs_dal.py b/src/database/dal/bot/bot_configs_dal.py
index adeb9ca..0d69d04 100644
--- a/src/database/dal/bot/bot_configs_dal.py
+++ b/src/database/dal/bot/bot_configs_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.bot_models import BotConfigs
diff --git a/src/database/dal/bot/custom_commands_dal.py b/src/database/dal/bot/custom_commands_dal.py
index f0f8aad..c500fd2 100644
--- a/src/database/dal/bot/custom_commands_dal.py
+++ b/src/database/dal/bot/custom_commands_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.bot_models import CustomCommands
diff --git a/src/database/dal/bot/dice_rolls_dal.py b/src/database/dal/bot/dice_rolls_dal.py
index 5960379..74b83e7 100644
--- a/src/database/dal/bot/dice_rolls_dal.py
+++ b/src/database/dal/bot/dice_rolls_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.bot_models import DiceRolls
diff --git a/src/database/dal/bot/profanity_filters_dal.py b/src/database/dal/bot/profanity_filters_dal.py
index 33e2cf6..26442ef 100644
--- a/src/database/dal/bot/profanity_filters_dal.py
+++ b/src/database/dal/bot/profanity_filters_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.bot_models import ProfanityFilters
diff --git a/src/database/dal/bot/servers_dal.py b/src/database/dal/bot/servers_dal.py
index 20665ad..e7e3f66 100644
--- a/src/database/dal/bot/servers_dal.py
+++ b/src/database/dal/bot/servers_dal.py
@@ -1,6 +1,6 @@
import discord
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.future import select
from src.database.models.bot_models import ProfanityFilters, Servers
diff --git a/src/database/dal/gw2/gw2_configs_dal.py b/src/database/dal/gw2/gw2_configs_dal.py
index cf1120c..a5dadca 100644
--- a/src/database/dal/gw2/gw2_configs_dal.py
+++ b/src/database/dal/gw2/gw2_configs_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.gw2_models import Gw2Configs
diff --git a/src/database/dal/gw2/gw2_key_dal.py b/src/database/dal/gw2/gw2_key_dal.py
index ed351cf..db2988f 100644
--- a/src/database/dal/gw2/gw2_key_dal.py
+++ b/src/database/dal/gw2/gw2_key_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.gw2_models import Gw2Keys
diff --git a/src/database/dal/gw2/gw2_session_chars_dal.py b/src/database/dal/gw2/gw2_session_chars_dal.py
index 6f350f9..23b010e 100644
--- a/src/database/dal/gw2/gw2_session_chars_dal.py
+++ b/src/database/dal/gw2/gw2_session_chars_dal.py
@@ -1,4 +1,4 @@
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy import update
from sqlalchemy.future import select
from src.database.models.gw2_models import Gw2SessionCharDeaths
diff --git a/src/database/dal/gw2/gw2_sessions_dal.py b/src/database/dal/gw2/gw2_sessions_dal.py
index 07fb600..070f366 100644
--- a/src/database/dal/gw2/gw2_sessions_dal.py
+++ b/src/database/dal/gw2/gw2_sessions_dal.py
@@ -1,5 +1,5 @@
import sqlalchemy as sa
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.future import select
from src.database.models.gw2_models import Gw2SessionCharDeaths, Gw2Sessions
diff --git a/src/database/migrations/env.py b/src/database/migrations/env.py
index 6912b0b..6cc9796 100644
--- a/src/database/migrations/env.py
+++ b/src/database/migrations/env.py
@@ -1,6 +1,6 @@
from alembic import context
from alembic.script import ScriptDirectory
-from ddcDatabases import get_postgresql_settings
+from ddcdatabases import get_postgresql_settings
from logging.config import fileConfig
from sqlalchemy import create_engine, engine_from_config, pool, text
from sqlalchemy.schema import SchemaItem
diff --git a/src/database/migrations/versions/0001_create_functions.py b/src/database/migrations/versions/0001_create_functions.py
index 6f5c75b..ccf4dc7 100644
--- a/src/database/migrations/versions/0001_create_functions.py
+++ b/src/database/migrations/versions/0001_create_functions.py
@@ -8,7 +8,7 @@
from alembic import op
from collections.abc import Sequence
-from ddcDatabases.postgresql import get_postgresql_settings
+from ddcdatabases.postgresql import get_postgresql_settings
revision: str = "0001"
down_revision: str | None = None
diff --git a/src/gw2/cogs/key.py b/src/gw2/cogs/key.py
index c79d064..ca90638 100644
--- a/src/gw2/cogs/key.py
+++ b/src/gw2/cogs/key.py
@@ -362,7 +362,7 @@ async def info(ctx):
is_valid_key = await gw2_api.check_api_key(api_key)
if not isinstance(is_valid_key, dict):
is_valid_key = "NO"
- name = f"***{gw2_messages.INVALID_API_KEY}***"
+ name = f"***{gw2_messages.INVALID_APIKEY_MSG}***"
else:
is_valid_key = "YES"
name = f"{ctx.message.author}"
diff --git a/src/gw2/constants/gw2_messages.py b/src/gw2/constants/gw2_messages.py
index 846a0c6..02ad5e4 100644
--- a/src/gw2/constants/gw2_messages.py
+++ b/src/gw2/constants/gw2_messages.py
@@ -7,7 +7,7 @@
#################################
# GW2 API
#################################
-INVALID_API_KEY = "This API Key is INVALID or no longer exists in gw2 api database"
+INVALID_APIKEY_MSG = "This API Key is INVALID or no longer exists in gw2 api database"
API_ERROR = "GW2 API ERROR"
API_DOWN = "GW2 API is currently down. Try again later."
API_NOT_FOUND = "GW2 API Not found."
diff --git a/src/gw2/tools/gw2_client.py b/src/gw2/tools/gw2_client.py
index def2187..5ee8236 100644
--- a/src/gw2/tools/gw2_client.py
+++ b/src/gw2/tools/gw2_client.py
@@ -108,13 +108,13 @@ async def _handle_api_error(self, response, endpoint):
def _handle_400_error(self, status, err_msg, init_msg):
"""Handle 400 Bad Request errors."""
if err_msg == "invalid key":
- raise APIInvalidKey(self.bot, f"({status}) {gw2_messages.INVALID_API_KEY}")
+ raise APIInvalidKey(self.bot, f"({status}) {gw2_messages.INVALID_APIKEY_MSG}")
raise APIBadRequest(self.bot, f"({init_msg}) {gw2_messages.API_DOWN}")
def _handle_403_error(self, status, err_msg, init_msg):
"""Handle 403 Forbidden errors."""
if err_msg == "invalid key":
- raise APIInvalidKey(self.bot, f"({status}) {gw2_messages.INVALID_API_KEY}")
+ raise APIInvalidKey(self.bot, f"({status}) {gw2_messages.INVALID_APIKEY_MSG}")
raise APIForbidden(self.bot, f"({init_msg}) {gw2_messages.API_ACCESS_DENIED}")
def _handle_404_error(self, status, endpoint):
diff --git a/src/gw2/tools/gw2_utils.py b/src/gw2/tools/gw2_utils.py
index 22b74f9..a378f8b 100644
--- a/src/gw2/tools/gw2_utils.py
+++ b/src/gw2/tools/gw2_utils.py
@@ -36,7 +36,7 @@ def __init__(self):
class Gw2Servers(Enum):
Anvil_Rock = "Anvil Rock"
- Borlis_Pass = "Borlis Pass"
+ Borlis_Pass = "Borlis Pass" # noqa: S105
Yaks_Bend = "Yak's Bend"
Henge_of_Denravi = "Henge of Denravi"
Maguuma = "Maguuma"
diff --git a/tests/docker/conftest.py b/tests/docker/conftest.py
index d464e8f..2083927 100644
--- a/tests/docker/conftest.py
+++ b/tests/docker/conftest.py
@@ -6,6 +6,7 @@
PROJECT_ROOT = Path(__file__).resolve().parents[2]
COMPOSE_FILE = "docker-compose.yml"
IMAGE_NAME = "discordbot-docker-test"
+DOCKER = shutil.which("docker") or "docker"
@pytest.fixture(scope="session")
@@ -27,7 +28,7 @@ def image_name():
def docker_build(project_root, image_name):
"""Build the python-base stage once per session."""
result = subprocess.run(
- ["docker", "build", "--target", "python-base", "-t", image_name, "."],
+ [DOCKER, "build", "--target", "python-base", "-t", image_name, "."],
cwd=project_root,
capture_output=True,
text=True,
@@ -35,7 +36,7 @@ def docker_build(project_root, image_name):
)
assert result.returncode == 0, f"Docker build failed:\n{result.stderr}"
yield image_name
- subprocess.run(["docker", "rmi", "-f", image_name], capture_output=True)
+ subprocess.run([DOCKER, "rmi", "-f", image_name], capture_output=True)
def pytest_collection_modifyitems(config, items):
diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py
index 38a922d..89ca1e4 100644
--- a/tests/docker/test_docker.py
+++ b/tests/docker/test_docker.py
@@ -3,6 +3,7 @@
import subprocess
pytestmark = pytest.mark.docker
+_DOCKER = shutil.which("docker") or "docker"
class TestDockerLint:
@@ -10,7 +11,7 @@ def test_hadolint_dockerfile(self, project_root):
"""Dockerfile passes hadolint linting."""
dockerfile = project_root / "Dockerfile"
hadolint_config = project_root / ".hadolint.yml"
- cmd = ["docker", "run", "--rm", "-i"]
+ cmd = [_DOCKER, "run", "--rm", "-i"]
if hadolint_config.exists():
cmd += ["-v", f"{hadolint_config}:/.config/hadolint.yaml:ro"]
cmd.append("hadolint/hadolint")
@@ -35,7 +36,7 @@ def test_compose_config(self, project_root, compose_file):
cleanup = True
try:
result = subprocess.run(
- ["docker", "compose", "-f", compose_file, "config", "--quiet"],
+ [_DOCKER, "compose", "-f", compose_file, "config", "--quiet"],
cwd=project_root,
capture_output=True,
text=True,
@@ -65,7 +66,7 @@ class TestDockerSmoke:
def test_python_available(self, docker_build):
"""Python is available and is 3.14.x in the base image."""
result = subprocess.run(
- ["docker", "run", "--rm", docker_build, "python", "--version"],
+ [_DOCKER, "run", "--rm", docker_build, "python", "--version"],
capture_output=True,
text=True,
timeout=30,
@@ -76,7 +77,7 @@ def test_python_available(self, docker_build):
def test_uv_available(self, docker_build):
"""uv is available in the base image."""
result = subprocess.run(
- ["docker", "run", "--rm", docker_build, "uv", "--version"],
+ [_DOCKER, "run", "--rm", docker_build, "uv", "--version"],
capture_output=True,
text=True,
timeout=30,
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 23d7893..b70777f 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -52,7 +52,7 @@ def setup_env_and_run_migrations(postgres_container):
os.environ["POSTGRESQL_SCHEMA"] = "gw2,public"
os.environ["POSTGRESQL_SSL_MODE"] = "disable"
- from ddcDatabases import clear_postgresql_settings_cache
+ from ddcdatabases import clear_postgresql_settings_cache
clear_postgresql_settings_cache()
diff --git a/tests/integration/test_alembic_migrations.py b/tests/integration/test_alembic_migrations.py
index 2794cc9..45e793d 100644
--- a/tests/integration/test_alembic_migrations.py
+++ b/tests/integration/test_alembic_migrations.py
@@ -39,14 +39,14 @@
async def _fetch_rows(db_session, stmt):
- from ddcDatabases import DBUtilsAsync
+ from ddcdatabases import DBUtilsAsync
db = DBUtilsAsync(db_session)
return await db.fetchall(stmt, True)
async def _execute(db_session, stmt):
- from ddcDatabases import DBUtilsAsync
+ from ddcdatabases import DBUtilsAsync
db = DBUtilsAsync(db_session)
await db.execute(stmt)
@@ -590,8 +590,8 @@ async def test_gw2_session_char_deaths_insert_and_read(db_session):
text(
"INSERT INTO gw2.gw2_session_char_deaths "
"(session_id, user_id, name, profession, start) "
- f"VALUES ('{session_id}', 8002, 'MyWarrior', 'Warrior', 5)"
- ),
+ "VALUES (:session_id, 8002, 'MyWarrior', 'Warrior', 5)"
+ ).bindparams(session_id=session_id),
)
rows = await _fetch_rows(
db_session,
diff --git a/tests/integration/test_gw2_api_public.py b/tests/integration/test_gw2_api_public.py
index 5911600..c19644d 100644
--- a/tests/integration/test_gw2_api_public.py
+++ b/tests/integration/test_gw2_api_public.py
@@ -6,6 +6,7 @@
import json
import pytest
+import urllib.parse
import urllib.request
from src.gw2.constants.gw2_currencies import ACHIEVEMENT_MAPPING, WALLET_MAPPING
@@ -15,8 +16,10 @@
def _fetch_json(url: str) -> dict | list:
"""Fetch JSON from a URL using only stdlib."""
- req = urllib.request.Request(url, headers={"User-Agent": "DiscordBot-Tests/1.0"})
- with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
+ if urllib.parse.urlparse(url).scheme not in ("http", "https"):
+ raise ValueError(f"Unsupported URL scheme: {url}")
+ req = urllib.request.Request(url, headers={"User-Agent": "DiscordBot-Tests/1.0"}) # noqa: S310
+ with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: # noqa: S310
return json.loads(resp.read().decode())
diff --git a/tests/integration/test_gw2_session_char_deaths_dal.py b/tests/integration/test_gw2_session_char_deaths_dal.py
index 763501e..a537083 100644
--- a/tests/integration/test_gw2_session_char_deaths_dal.py
+++ b/tests/integration/test_gw2_session_char_deaths_dal.py
@@ -1,5 +1,5 @@
import pytest
-from ddcDatabases import DBUtilsAsync
+from ddcdatabases import DBUtilsAsync
from sqlalchemy.exc import IntegrityError
from sqlalchemy.future import select
from src.database.dal.gw2.gw2_session_chars_dal import Gw2SessionCharDeathsDal
diff --git a/tests/unit/bot/constants/test_messages.py b/tests/unit/bot/constants/test_messages.py
index 2cdc80e..a969e49 100644
--- a/tests/unit/bot/constants/test_messages.py
+++ b/tests/unit/bot/constants/test_messages.py
@@ -25,7 +25,7 @@ class TestBotClass:
"""Test cases for Bot message class."""
def test_token_not_found(self):
- assert Bot.TOKEN_NOT_FOUND == "BOT_TOKEN variable not found"
+ assert Bot.MISSING_ENV_VAR == "BOT_TOKEN variable not found"
def test_terminated(self):
assert Bot.TERMINATED == "Bot has been terminated."
@@ -450,7 +450,7 @@ class TestBackwardCompatibility:
"""Test that module-level aliases match their class counterparts."""
def test_bot_constants(self):
- assert messages.BOT_TOKEN_NOT_FOUND == Bot.TOKEN_NOT_FOUND
+ assert messages.BOT_TOKEN_NOT_FOUND == Bot.MISSING_ENV_VAR
assert messages.BOT_TERMINATED == Bot.TERMINATED
assert messages.BOT_STOPPED_CTRTC == Bot.STOPPED_CTRTC
assert messages.BOT_FATAL_ERROR_MAIN == Bot.FATAL_ERROR_MAIN
diff --git a/tests/unit/bot/tools/test_custom_help.py b/tests/unit/bot/tools/test_custom_help.py
index 2f618b4..addd382 100644
--- a/tests/unit/bot/tools/test_custom_help.py
+++ b/tests/unit/bot/tools/test_custom_help.py
@@ -36,22 +36,22 @@ async def test_initial_state_two_pages(self):
assert view.page_indicator.label == "1/2"
@pytest.mark.asyncio
- async def test_format_page_first(self):
- """Test _format_page returns page header + content for first page."""
+ async def testformat_page_first(self):
+ """Test format_page returns page header + content for first page."""
view = HelpPaginatorView(["```\nContent A\n```", "```\nContent B\n```"], author_id=1)
- result = view._format_page()
+ result = view.format_page()
assert result == "**Page 1/2**\n```\nContent A\n```"
@pytest.mark.asyncio
- async def test_format_page_second(self):
- """Test _format_page returns correct content after navigating."""
+ async def testformat_page_second(self):
+ """Test format_page returns correct content after navigating."""
view = HelpPaginatorView(["Page A", "Page B", "Page C"], author_id=1)
view.current_page = 1
view._update_buttons()
- result = view._format_page()
+ result = view.format_page()
assert result == "**Page 2/3**\nPage B"
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index c0c1e8a..c3388ad 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -3,10 +3,13 @@
Auto-imports all modules for coverage discovery.
"""
+import logging
import sys
from pathlib import Path
from unittest.mock import Mock
+logger = logging.getLogger(__name__)
+
# Ensure the project root is on sys.path so `from src...` imports work
# regardless of whether pytest was invoked via `python -m pytest` or `pytest`
_project_root = str(Path(__file__).resolve().parent.parent.parent)
@@ -36,8 +39,7 @@ def auto_import_modules():
try:
__import__(module_name)
except Exception:
- # Silently ignore import failures
- pass
+ logger.debug("Failed to import %s for coverage discovery", module_name)
# Run auto-import during pytest collection
diff --git a/tests/unit/gw2/cogs/test_account.py b/tests/unit/gw2/cogs/test_account.py
index aaee876..cba8790 100644
--- a/tests/unit/gw2/cogs/test_account.py
+++ b/tests/unit/gw2/cogs/test_account.py
@@ -122,7 +122,7 @@ async def test_account_command_invalid_api_key(self, mock_ctx, sample_api_key_da
with patch("src.gw2.cogs.account.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_key_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_key_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_key_error)
with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error:
@@ -130,7 +130,7 @@ async def test_account_command_invalid_api_key(self, mock_ctx, sample_api_key_da
mock_error.assert_called_once()
error_msg = mock_error.call_args[0][1]
- assert gw2_messages.INVALID_API_KEY in error_msg
+ assert gw2_messages.INVALID_APIKEY_MSG in error_msg
@pytest.mark.asyncio
async def test_account_command_insufficient_permissions(self, mock_ctx, sample_account_data):
diff --git a/tests/unit/gw2/cogs/test_characters.py b/tests/unit/gw2/cogs/test_characters.py
index 3f068d1..1ce8aa1 100644
--- a/tests/unit/gw2/cogs/test_characters.py
+++ b/tests/unit/gw2/cogs/test_characters.py
@@ -112,14 +112,14 @@ async def test_characters_invalid_api_key_sends_error_with_help(self, mock_ctx,
mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data)
with patch("src.gw2.cogs.characters.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error)
with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error:
mock_error.return_value = None
await characters(mock_ctx)
mock_error.assert_called_once()
error_msg = mock_error.call_args[0][1]
- assert gw2_messages.INVALID_API_KEY in error_msg
+ assert gw2_messages.INVALID_APIKEY_MSG in error_msg
assert "gw2 key add" in error_msg or "key add" in error_msg
@pytest.mark.asyncio
diff --git a/tests/unit/gw2/cogs/test_key.py b/tests/unit/gw2/cogs/test_key.py
index 8dc0d92..cfd4883 100644
--- a/tests/unit/gw2/cogs/test_key.py
+++ b/tests/unit/gw2/cogs/test_key.py
@@ -105,7 +105,7 @@ async def test_add_deletes_message_for_privacy(self, mock_ctx):
with patch("src.gw2.cogs.key.bot_utils.delete_message") as mock_delete:
with patch("src.gw2.cogs.key.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error)
with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error:
mock_error.return_value = None
@@ -119,14 +119,14 @@ async def test_add_invalid_api_key_sends_error(self, mock_ctx):
with patch("src.gw2.cogs.key.bot_utils.delete_message"):
with patch("src.gw2.cogs.key.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error)
with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error:
mock_error.return_value = None
await add(mock_ctx, api_key)
mock_error.assert_called_once()
error_msg = mock_error.call_args[0][1]
- assert gw2_messages.INVALID_API_KEY in error_msg
+ assert gw2_messages.INVALID_APIKEY_MSG in error_msg
assert api_key in error_msg
@pytest.mark.asyncio
@@ -362,14 +362,14 @@ async def test_update_invalid_api_key_sends_error(self, mock_ctx):
mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}])
with patch("src.gw2.cogs.key.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error)
with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error:
mock_error.return_value = None
await update(mock_ctx, api_key)
mock_error.assert_called_once()
error_msg = mock_error.call_args[0][1]
- assert gw2_messages.INVALID_API_KEY in error_msg
+ assert gw2_messages.INVALID_APIKEY_MSG in error_msg
@pytest.mark.asyncio
async def test_update_account_info_api_fails(self, mock_ctx):
@@ -678,7 +678,7 @@ async def test_info_invalid_api_key_on_check_shows_no_valid(self, mock_ctx):
)
with patch("src.gw2.cogs.key.Gw2Client") as mock_client:
mock_client_instance = mock_client.return_value
- invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_API_KEY}")
+ invalid_error = APIInvalidKey(mock_ctx.bot, f"(400) {gw2_messages.INVALID_APIKEY_MSG}")
mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error)
with patch("src.gw2.cogs.key.bot_utils.send_embed") as mock_send:
with patch("src.gw2.cogs.key.bot_utils.get_current_date_time_str_long") as mock_time:
diff --git a/tests/unit/gw2/tools/test_gw2_client.py b/tests/unit/gw2/tools/test_gw2_client.py
index e5a91c0..5aa5542 100644
--- a/tests/unit/gw2/tools/test_gw2_client.py
+++ b/tests/unit/gw2/tools/test_gw2_client.py
@@ -540,7 +540,7 @@ def test_invalid_key_raises_api_invalid_key(self, gw2_client):
with pytest.raises(APIInvalidKey) as exc_info:
gw2_client._handle_400_error(400, "invalid key", "init_msg")
- assert gw2_messages.INVALID_API_KEY in str(exc_info.value)
+ assert gw2_messages.INVALID_APIKEY_MSG in str(exc_info.value)
def test_other_error_raises_api_bad_request(self, gw2_client):
"""Test other error messages raise APIBadRequest (line 90)."""
@@ -575,7 +575,7 @@ def test_invalid_key_raises_api_invalid_key(self, gw2_client):
with pytest.raises(APIInvalidKey) as exc_info:
gw2_client._handle_403_error(403, "invalid key", "init_msg")
- assert gw2_messages.INVALID_API_KEY in str(exc_info.value)
+ assert gw2_messages.INVALID_APIKEY_MSG in str(exc_info.value)
def test_other_error_raises_api_forbidden(self, gw2_client):
"""Test other error messages raise APIForbidden (line 96)."""
diff --git a/uv.lock b/uv.lock
index 3635f76..a1aea70 100644
--- a/uv.lock
+++ b/uv.lock
@@ -135,11 +135,11 @@ wheels = [
[[package]]
name = "attrs"
-version = "25.4.0"
+version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
@@ -310,54 +310,54 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.13.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
- { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
- { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
- { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
- { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
- { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
- { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
- { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
- { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
- { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
- { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
- { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
- { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
- { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
- { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
- { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
- { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
- { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
- { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
- { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
- { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
- { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
- { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
- { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
- { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
- { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
- { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
- { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
- { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
- { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
- { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
+version = "7.13.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
+ { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
+ { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
+ { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
+ { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
+ { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
]
[[package]]
name = "ddcdatabases"
-version = "3.0.11"
+version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic-settings" },
{ name = "sqlalchemy", extra = ["asyncio"] },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6f/c2/4978087ad2f17c58c90a65c4060e58db7228ea11e85a583bf44fc02457d4/ddcdatabases-3.0.11.tar.gz", hash = "sha256:67904e6fe84effbc8ce8a73fea94619288ce58a4ffc0bc2a9d329d644cf3333a", size = 38268, upload-time = "2026-02-25T22:37:54.071Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/2b/5366a265b047b3d35ba994e38219ea43ee7879ce4849c5943398f9dc2c61/ddcdatabases-4.0.1.tar.gz", hash = "sha256:5fa9bccbcd3335f8538a9f87e570426eed62033db2ad1f78f3187e2e28f50069", size = 38682, upload-time = "2026-03-20T16:20:14.004Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4b/b8/c9a052fa29036b6386da00910f7318a921a59f0c783ee20a1b5c2f620dc6/ddcdatabases-3.0.11-py3-none-any.whl", hash = "sha256:5d4cd4ade1439044ff79308f9e59fe11fae2b0334941be5b1625beb8ba98c426", size = 42277, upload-time = "2026-02-25T22:37:52.798Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/88/babefbb84382c784f408b63592a549d485c6b09d769ecba5ce9c0d5d3298/ddcdatabases-4.0.1-py3-none-any.whl", hash = "sha256:af3931f02637d910bddaddef13de8da4671b305ce3bb4e5a33e4294d4d329a37", size = 41118, upload-time = "2026-03-20T16:20:13.117Z" },
]
[package.optional-dependencies]
@@ -381,7 +381,7 @@ wheels = [
[[package]]
name = "discordbot"
-version = "3.0.8"
+version = "3.0.9"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },
@@ -410,22 +410,22 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.18.4" },
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
{ name = "better-profanity", specifier = ">=0.7.0" },
- { name = "ddcdatabases", extras = ["postgres"], specifier = ">=3.0.11" },
+ { name = "ddcdatabases", extras = ["postgres"], specifier = ">=4.0.1" },
{ name = "discord-py", specifier = ">=2.7.1" },
{ name = "gtts", specifier = ">=2.5.4" },
- { name = "openai", specifier = ">=2.28.0" },
+ { name = "openai", specifier = ">=2.29.0" },
{ name = "pynacl", specifier = ">=1.6.2" },
- { name = "pythonlogs", specifier = ">=6.0.3" },
+ { name = "pythonlogs", specifier = ">=7.0.0" },
{ name = "uuid-utils", specifier = ">=0.14.1" },
]
[package.metadata.requires-dev]
dev = [
- { name = "coverage", specifier = ">=7.13.4" },
+ { name = "coverage", specifier = ">=7.13.5" },
{ name = "poethepoet", specifier = ">=0.42.1" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
- { name = "ruff", specifier = ">=0.15.6" },
- { name = "testcontainers", extras = ["postgres"], specifier = ">=4.14.1" },
+ { name = "ruff", specifier = ">=0.15.7" },
+ { name = "testcontainers", extras = ["postgres"], specifier = ">=4.14.2" },
]
[[package]]
@@ -707,7 +707,7 @@ wheels = [
[[package]]
name = "openai"
-version = "2.28.0"
+version = "2.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -719,9 +719,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/56/87/eb0abb4ef88ddb95b3c13149384c4c288f584f3be17d6a4f63f8c3e3c226/openai-2.28.0.tar.gz", hash = "sha256:bb7fdff384d2a787fa82e8822d1dd3c02e8cf901d60f1df523b7da03cbb6d48d", size = 670334, upload-time = "2026-03-13T19:56:27.306Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c0/5a/df122348638885526e53140e9c6b0d844af7312682b3bde9587eebc28b47/openai-2.28.0-py3-none-any.whl", hash = "sha256:79aa5c45dba7fef84085701c235cf13ba88485e1ef4f8dfcedc44fc2a698fc1d", size = 1141218, upload-time = "2026-03-13T19:56:25.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" },
]
[[package]]
@@ -998,14 +998,14 @@ wheels = [
[[package]]
name = "pythonlogs"
-version = "6.0.3"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic-settings" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/47/22/4cc8022d2cde5933a3c634e25b7facf6b1f520130ce6f74ffe3629c2c43b/pythonlogs-6.0.3.tar.gz", hash = "sha256:144da03ca5f555f1f4aae8e298d1c465b73fc15e3b6fe141cf4e2b20f2f52ba8", size = 21278, upload-time = "2026-02-25T22:30:12.033Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/00/f2366834681f9d66b670aed50f0931d37c4241fe43d1e4cddb0c2b70ce9b/pythonlogs-7.0.0.tar.gz", hash = "sha256:8a5ef7a4508cd718772d2dd7052b50c0b44cafe72b52a5cc3e382f8efb6a15e8", size = 21406, upload-time = "2026-03-20T14:21:58.886Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/35/bf851f6443797d1d0ae7fcd1b808a94093eb26e294261386f1d0ecd92116/pythonlogs-6.0.3-py3-none-any.whl", hash = "sha256:3227497d61412c90ca17a56537373c1fa346f460b940a43337f0349650c8fb71", size = 25224, upload-time = "2026-02-25T22:30:11.167Z" },
+ { url = "https://files.pythonhosted.org/packages/83/c4/40a854e28beaf1201a291fb8ac7624184f0e5427506f34874c4f51a96bda/pythonlogs-7.0.0-py3-none-any.whl", hash = "sha256:61ee53e710edd7aedb7d21340c591e5d2cfd9a4dab5aae62914501299a704d0d", size = 25308, upload-time = "2026-03-20T14:21:57.614Z" },
]
[[package]]
@@ -1061,27 +1061,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
- { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
- { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
- { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
- { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
- { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
- { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
- { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
- { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
- { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
- { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
- { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
- { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
- { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
- { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
- { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
+version = "0.15.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
+ { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
+ { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
]
[[package]]
@@ -1135,7 +1135,7 @@ asyncio = [
[[package]]
name = "testcontainers"
-version = "4.14.1"
+version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docker" },
@@ -1144,9 +1144,9 @@ dependencies = [
{ name = "urllib3" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/02/ef62dec9e4f804189c44df23f0b86897c738d38e9c48282fcd410308632f/testcontainers-4.14.1.tar.gz", hash = "sha256:316f1bb178d829c003acd650233e3ff3c59a833a08d8661c074f58a4fbd42a64", size = 80148, upload-time = "2026-01-31T23:13:46.915Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/ac/a597c3a0e02b26cbed6dd07df68be1e57684766fd1c381dee9b170a99690/testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239", size = 166841, upload-time = "2026-03-18T05:19:16.696Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl", hash = "sha256:03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891", size = 125640, upload-time = "2026-01-31T23:13:45.464Z" },
+ { url = "https://files.pythonhosted.org/packages/13/2d/26b8b30067d94339afee62c3edc9b803a6eb9332f521ba77d8aaab5de873/testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68", size = 125712, upload-time = "2026-03-18T05:19:15.29Z" },
]
[[package]]