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 @@

Sponsor
- Ko-fi + Ko-fi Donate
Python @@ -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) +Sponsor on GitHub +Buy Me a Coffee at ko-fi.com +Donate via PayPal 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]]