diff --git a/.env.example b/.env.example index e405a7a..2f0e65a 100644 --- a/.env.example +++ b/.env.example @@ -17,9 +17,17 @@ BOT_OPENAI_COOLDOWN=10 BOT_OWNER_COOLDOWN=5 -# OpenAI API key +# OpenAI (GPT) API key BOT_OPENAI_MODEL=gpt-5.5 -OPENAI_API_KEY= +BOT_OPENAI_API_KEY= + +# Anthropic (Claude) API key +BOT_ANTHROPIC_MODEL=claude-opus-4-8 +BOT_ANTHROPIC_API_KEY= + +# Google (Gemini) API key +BOT_GEMINI_MODEL=gemini-flash-latest +BOT_GEMINI_API_KEY= # ddcDatabases configs diff --git a/pyproject.toml b/pyproject.toml index f184f12..b339dff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "DiscordBot" -version = "3.0.16" +version = "3.0.17" 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" @@ -31,10 +31,12 @@ classifiers = [ requires-python = ">=3.14" dependencies = [ "alembic>=1.18.4", + "anthropic>=0.105.2", "beautifulsoup4>=4.14.3", "better-profanity>=0.7.0", "ddcdatabases[postgres]>=4.0.1", "discord-py>=2.7.1", + "google-genai>=2.7.0", "gTTS>=2.5.4", "openai>=2.38.0", "PyNaCl>=1.6.2", diff --git a/src/bot/cogs/open_ai.py b/src/bot/cogs/open_ai.py index c7132c8..1671e70 100644 --- a/src/bot/cogs/open_ai.py +++ b/src/bot/cogs/open_ai.py @@ -1,6 +1,9 @@ import discord import time +from anthropic import AsyncAnthropic from discord.ext import commands +from google import genai +from google.genai import types as genai_types from openai import AsyncOpenAI from openai.types.responses import WebSearchToolParam from openai.types.shared import ReasoningEffort @@ -12,12 +15,14 @@ class OpenAi(commands.Cog): - """OpenAI-powered commands for AI assistance and text generation.""" + """LLM chat commands (OpenAI / Anthropic Claude / Google Gemini) with optional web search.""" def __init__(self, bot: Bot) -> None: self.bot: Bot = bot self._bot_settings: BotSettings = get_bot_settings() self._openai_client: AsyncOpenAI = AsyncOpenAI(api_key=self._bot_settings.openai_api_key) + self._anthropic_client: AsyncAnthropic = AsyncAnthropic(api_key=self._bot_settings.anthropic_api_key) + self._gemini_client: genai.Client = genai.Client(api_key=self._bot_settings.gemini_api_key) self._effort: ReasoningEffort = "xhigh" self._instructions: str = "You are a helpful AI assistant." self._instructions_web: str = ( @@ -26,33 +31,54 @@ def __init__(self, bot: Bot) -> None: "numbers, statistics, or breakdowns that the sources do not explicitly state. Cite the source URL(s)." ) + # ─────────────────────────── Commands ─────────────────────────── + + @commands.command() + @commands.guild_only() + @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) + async def gpt(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask OpenAI's GPT model for a direct answer (no web search).""" + await self._run_chat(ctx, msg_text, provider="openai", use_web=False) + + @commands.command() + @commands.guild_only() + @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) + async def gptweb(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask OpenAI's GPT model with web search enabled — for current/factual info.""" + await self._run_chat(ctx, msg_text, provider="openai", use_web=True) + + @commands.command() + @commands.guild_only() + @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) + async def claude(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask Anthropic's Claude model for a direct answer (no web search).""" + await self._run_chat(ctx, msg_text, provider="anthropic", use_web=False) + @commands.command() + @commands.guild_only() @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) - async def ai(self, ctx: commands.Context, *, msg_text: str) -> None: - """Ask OpenAI for a direct answer (no web search). + async def claudeweb(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask Anthropic's Claude model with web search enabled — for current/factual info.""" + await self._run_chat(ctx, msg_text, provider="anthropic", use_web=True) - Usage: - ai What is Python? - ai Write a haiku about programming - ai Explain quantum computing in simple terms - """ - await self._run_ai(ctx, msg_text, use_web=False) + @commands.command() + @commands.guild_only() + @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) + async def gemini(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask Google's Gemini model for a direct answer (no web search).""" + await self._run_chat(ctx, msg_text, provider="gemini", use_web=False) @commands.command() + @commands.guild_only() @commands.cooldown(1, CoolDowns.OpenAI.value, commands.BucketType.user) - async def aiweb(self, ctx: commands.Context, *, msg_text: str) -> None: - """Ask OpenAI to search the web before answering — best for current/factual info. - - Usage: - aiweb What's the latest news about - aiweb How many support gems does Path of Exile 2 have - """ - await self._run_ai(ctx, msg_text, use_web=True) - - async def _run_ai(self, ctx: commands.Context, msg_text: str, use_web: bool) -> None: - """Shared body for the `ai` and `aiweb` commands.""" - # Reasoning (and web search) can take a couple of minutes, so show a progress - # message immediately so the user knows the bot is working (not stuck). + async def geminiweb(self, ctx: commands.Context, *, msg_text: str) -> None: + """Ask Google's Gemini model with Google Search grounding enabled.""" + await self._run_chat(ctx, msg_text, provider="gemini", use_web=True) + + # ─────────────────────────── Shared flow ─────────────────────────── + + async def _run_chat(self, ctx: commands.Context, msg_text: str, provider: str, use_web: bool) -> None: + """Send progress message, dispatch to provider, time, post answer.""" progress_text = ( "Please wait, I'm thinking and searching the web for an accurate answer..." if use_web @@ -67,38 +93,50 @@ async def _run_ai(self, ctx: commands.Context, msg_text: str, use_web: bool) -> start = time.monotonic() try: - response_text = await self._get_ai_response(msg_text, use_web=use_web) + response_text = await self._dispatch(provider, msg_text, use_web) color = discord.Color.green() description = response_text except Exception as e: - self.bot.log.error(f"OpenAI API error: {e}") + self.bot.log.error(f"{provider} API error: {e}") color = discord.Color.red() description = f"Sorry, I encountered an error: {e}" elapsed = time.monotonic() - start - # Remove the progress message before sending the final answer. try: await progress_msg.delete() except discord.HTTPException: pass - embeds = self._create_ai_embeds(ctx, description, color, elapsed) + model = self._model_for(provider) + embeds = self._create_ai_embeds(ctx, description, color, elapsed, model) if len(embeds) == 1: await bot_utils.send_embed(ctx, embeds[0], False) else: view = bot_utils.EmbedPaginatorView(embeds, ctx.author.id) await view.send_and_save(ctx) - async def _get_ai_response(self, message: str, use_web: bool) -> str: - """Get response from OpenAI API. - - use_web: when True, enables the built-in web_search tool and uses the - web-grounded instructions. When False, the model answers from its own - knowledge with plain instructions. - """ + async def _dispatch(self, provider: str, message: str, use_web: bool) -> str: + if provider == "openai": + return await self._get_openai_response(message, use_web=use_web) + if provider == "anthropic": + return await self._get_claude_response(message, use_web=use_web) + if provider == "gemini": + return await self._get_gemini_response(message, use_web=use_web) + raise ValueError(f"Unknown provider: {provider}") + + def _model_for(self, provider: str) -> str: + return { + "openai": self._bot_settings.openai_model, + "anthropic": self._bot_settings.anthropic_model, + "gemini": self._bot_settings.gemini_model, + }[provider] + + # ─────────────────────────── Provider calls ─────────────────────────── + + async def _get_openai_response(self, message: str, use_web: bool) -> str: + """Call OpenAI's Responses API with optional web_search tool.""" instructions = self._instructions_web if use_web else self._instructions tools: list[WebSearchToolParam] = [WebSearchToolParam(type="web_search")] if use_web else [] - response = await self._openai_client.responses.create( instructions=instructions, model=self._bot_settings.openai_model, @@ -107,10 +145,44 @@ async def _get_ai_response(self, message: str, use_web: bool) -> str: max_output_tokens=None, input=message, ) - content = response.output_text return content.strip() if content else "" + async def _get_claude_response(self, message: str, use_web: bool) -> str: + """Call Anthropic Messages API with optional web_search server tool.""" + instructions = self._instructions_web if use_web else self._instructions + tools = [{"type": "web_search_20250305", "name": "web_search"}] if use_web else [] + response = await self._anthropic_client.messages.create( + model=self._bot_settings.anthropic_model, + max_tokens=4096, + system=instructions, + messages=[{"role": "user", "content": message}], + tools=tools, + ) + # Concatenate all text blocks (web search may interleave tool-use blocks). + text_parts = [ + getattr(block, "text", "") for block in response.content if getattr(block, "type", None) == "text" + ] + content = "".join(text_parts).strip() + return content + + async def _get_gemini_response(self, message: str, use_web: bool) -> str: + """Call Google Gemini with optional Google Search grounding.""" + instructions = self._instructions_web if use_web else self._instructions + config_kwargs: dict = {"system_instruction": instructions} + if use_web: + config_kwargs["tools"] = [genai_types.Tool(google_search=genai_types.GoogleSearch())] + config = genai_types.GenerateContentConfig(**config_kwargs) + response = await self._gemini_client.aio.models.generate_content( + model=self._bot_settings.gemini_model, + contents=message, + config=config, + ) + content = response.text or "" + return content.strip() + + # ─────────────────────────── Embed formatting ─────────────────────────── + @staticmethod def _format_duration(seconds: float) -> str: """Format an elapsed duration as e.g. '5ms' (sub-second) or '20s'.""" @@ -119,13 +191,19 @@ def _format_duration(seconds: float) -> str: return f"{round(seconds)}s" def _create_ai_embeds( - self, ctx: commands.Context, description: str, color: discord.Color, elapsed: float = 0.0 + self, + ctx: commands.Context, + description: str, + color: discord.Color, + elapsed: float = 0.0, + model: str | None = None, ) -> list[discord.Embed]: - """Create formatted embed(s) for AI response, paginating if needed.""" - model = self._bot_settings.openai_model + """Create formatted embed(s) for an AI response, paginating if needed.""" + if model is None: + model = self._bot_settings.openai_model duration = self._format_duration(elapsed) max_length = 2000 - chunks = [] + chunks: list[str] = [] while description: if len(description) <= max_length: diff --git a/src/bot/constants/settings.py b/src/bot/constants/settings.py index 3f21b86..41c5f98 100644 --- a/src/bot/constants/settings.py +++ b/src/bot/constants/settings.py @@ -26,6 +26,16 @@ class BotSettings(BaseSettings): openai_model: str = Field(default="gpt-5.5", description="https://developers.openai.com/api/docs/models") openai_api_key: str | None = Field(default=None) + # Anthropic (Claude) + anthropic_model: str = Field( + default="claude-opus-4-8", description="https://docs.anthropic.com/en/docs/about-claude/models" + ) + anthropic_api_key: str | None = Field(default=None) + + # Google (Gemini) + gemini_model: str = Field(default="gemini-flash-latest", description="https://ai.google.dev/gemini-api/docs/models") + gemini_api_key: str | None = Field(default=None) + # Cooldowns admin_cooldown: int = Field(default=20) config_cooldown: int = Field(default=20) diff --git a/tests/unit/bot/cogs/test_open_ai.py b/tests/unit/bot/cogs/test_open_ai.py index 3e575fa..9482068 100644 --- a/tests/unit/bot/cogs/test_open_ai.py +++ b/tests/unit/bot/cogs/test_open_ai.py @@ -28,8 +28,20 @@ def mock_bot(): @pytest.fixture def openai_cog(mock_bot): """Create an OpenAi cog instance.""" - with patch("src.bot.cogs.open_ai.get_bot_settings") as mock_settings, patch("src.bot.cogs.open_ai.AsyncOpenAI"): - mock_settings.return_value = MagicMock(openai_api_key="test-key", openai_model="gpt-3.5-turbo") + with ( + patch("src.bot.cogs.open_ai.get_bot_settings") as mock_settings, + patch("src.bot.cogs.open_ai.AsyncOpenAI"), + patch("src.bot.cogs.open_ai.AsyncAnthropic"), + patch("src.bot.cogs.open_ai.genai.Client"), + ): + mock_settings.return_value = MagicMock( + openai_api_key="test-key", + openai_model="gpt-3.5-turbo", + anthropic_api_key="test-anthropic-key", + anthropic_model="claude-test", + gemini_api_key="test-gemini-key", + gemini_model="gemini-test", + ) return OpenAi(mock_bot) @@ -93,8 +105,8 @@ async def test_ai_command_success( """Test successful AI command execution.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, "_get_ai_response", return_value="AI response here"): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="What is Python?") + with patch.object(openai_cog, "_get_openai_response", return_value="AI response here"): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="What is Python?") mock_ctx.send.assert_called_once() # progress message was sent mock_send_embed.assert_called_once() @@ -115,8 +127,8 @@ async def test_aiweb_command_success( """`aiweb` runs the shared flow with use_web=True (web search enabled).""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, "_get_ai_response", return_value="web answer") as mock_get_response: - await openai_cog.aiweb.callback(openai_cog, mock_ctx, msg_text="Latest news") + with patch.object(openai_cog, "_get_openai_response", return_value="web answer") as mock_get_response: + await openai_cog.gptweb.callback(openai_cog, mock_ctx, msg_text="Latest news") mock_get_response.assert_awaited_once_with("Latest news", use_web=True) mock_ctx.send.assert_called_once() # progress message @@ -131,14 +143,193 @@ async def test_aiweb_command_success( async def test_ai_command_uses_plain_path( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings ): - """`ai` routes through _get_ai_response with use_web=False.""" + """`ai` routes through _get_openai_response with use_web=False.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, "_get_ai_response", return_value="plain answer") as mock_get_response: - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="What is Python?") + with patch.object(openai_cog, "_get_openai_response", return_value="plain answer") as mock_get_response: + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="What is Python?") mock_get_response.assert_awaited_once_with("What is Python?", use_web=False) + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_claude_command_routes_to_anthropic_plain( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """`claude` runs _get_claude_response with use_web=False.""" + mock_get_settings.return_value = mock_bot_settings + with patch.object(openai_cog, "_get_claude_response", return_value="claude answer") as mock_get_response: + await openai_cog.claude.callback(openai_cog, mock_ctx, msg_text="hi") + mock_get_response.assert_awaited_once_with("hi", use_web=False) + mock_send_embed.assert_called_once() + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_claudeweb_command_routes_to_anthropic_web( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """`claudeweb` runs _get_claude_response with use_web=True.""" + mock_get_settings.return_value = mock_bot_settings + with patch.object(openai_cog, "_get_claude_response", return_value="claude web answer") as mock_get_response: + await openai_cog.claudeweb.callback(openai_cog, mock_ctx, msg_text="news") + mock_get_response.assert_awaited_once_with("news", use_web=True) + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_gemini_command_routes_to_gemini_plain( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """`gemini` runs _get_gemini_response with use_web=False.""" + mock_get_settings.return_value = mock_bot_settings + with patch.object(openai_cog, "_get_gemini_response", return_value="gemini answer") as mock_get_response: + await openai_cog.gemini.callback(openai_cog, mock_ctx, msg_text="hi") + mock_get_response.assert_awaited_once_with("hi", use_web=False) + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_geminiweb_command_routes_to_gemini_web( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """`geminiweb` runs _get_gemini_response with use_web=True.""" + mock_get_settings.return_value = mock_bot_settings + with patch.object(openai_cog, "_get_gemini_response", return_value="gemini web answer") as mock_get_response: + await openai_cog.geminiweb.callback(openai_cog, mock_ctx, msg_text="news") + mock_get_response.assert_awaited_once_with("news", use_web=True) + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_progress_delete_failure_is_tolerated( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """If progress_msg.delete() raises discord.HTTPException, _run_chat still posts the answer.""" + import discord + + mock_get_settings.return_value = mock_bot_settings + # Make ctx.send return a message whose .delete() raises HTTPException. + progress_msg = MagicMock() + progress_msg.delete = AsyncMock(side_effect=discord.HTTPException(MagicMock(), "boom")) + mock_ctx.send = AsyncMock(return_value=progress_msg) + + with patch.object(openai_cog, "_get_openai_response", return_value="hi"): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="hi") + + # Final answer was still sent via send_embed despite the delete failure. + mock_send_embed.assert_called_once() + # And we attempted the delete. + progress_msg.delete.assert_awaited_once() + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_footer_carries_active_provider_model( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """Embed footer reflects the active provider's model, not OpenAI's by default.""" + mock_get_settings.return_value = mock_bot_settings + with patch.object(openai_cog, "_get_claude_response", return_value="claude says hi"): + await openai_cog.claude.callback(openai_cog, mock_ctx, msg_text="hi") + + embed = mock_send_embed.call_args[0][1] + # mock_bot_settings.anthropic_model = "claude-test" (per the openai_cog fixture) + assert "claude-test" in embed.footer.text + + @pytest.mark.asyncio + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") + async def test_progress_message_text_differs_by_use_web( + self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings + ): + """Web-enabled commands show 'searching the web' in the progress embed; plain ones don't.""" + mock_get_settings.return_value = mock_bot_settings + + with patch.object(openai_cog, "_get_openai_response", return_value="x"): + # Plain: progress text should NOT mention web search. + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="hi") + plain_embed = mock_ctx.send.call_args[1]["embed"] + assert "searching the web" not in plain_embed.description.lower() + + mock_ctx.send.reset_mock() + mock_send_embed.reset_mock() + + # Web: progress text SHOULD mention web search. + await openai_cog.gptweb.callback(openai_cog, mock_ctx, msg_text="hi") + web_embed = mock_ctx.send.call_args[1]["embed"] + assert "searching the web" in web_embed.description.lower() + + @pytest.mark.asyncio + async def test_dispatch_unknown_provider_raises(self, openai_cog): + """_dispatch raises ValueError on unknown provider.""" + with pytest.raises(ValueError): + await openai_cog._dispatch("notaprovider", "hi", False) + + @pytest.mark.asyncio + async def test_get_claude_response_concatenates_text_blocks(self, openai_cog): + """_get_claude_response joins text blocks and strips.""" + block_text = MagicMock(type="text", text="part one ") + block_tool = MagicMock(type="tool_use", text="ignored") + block_text2 = MagicMock(type="text", text="part two") + mock_response = MagicMock(content=[block_text, block_tool, block_text2]) + openai_cog._anthropic_client = MagicMock() + openai_cog._anthropic_client.messages.create = AsyncMock(return_value=mock_response) + + result = await openai_cog._get_claude_response("hi", use_web=False) + + assert result == "part one part two" + call_args = openai_cog._anthropic_client.messages.create.call_args[1] + assert call_args["model"] == "claude-test" + assert call_args["system"] == openai_cog._instructions + assert call_args["tools"] == [] + assert call_args["messages"] == [{"role": "user", "content": "hi"}] + + @pytest.mark.asyncio + async def test_get_claude_response_web_enables_tool(self, openai_cog): + """use_web=True enables the web_search server tool and uses web instructions.""" + block_text = MagicMock(type="text", text="ok") + mock_response = MagicMock(content=[block_text]) + openai_cog._anthropic_client = MagicMock() + openai_cog._anthropic_client.messages.create = AsyncMock(return_value=mock_response) + + await openai_cog._get_claude_response("hi", use_web=True) + + call_args = openai_cog._anthropic_client.messages.create.call_args[1] + assert call_args["tools"] == [{"type": "web_search_20250305", "name": "web_search"}] + assert call_args["system"] == openai_cog._instructions_web + + @pytest.mark.asyncio + async def test_get_gemini_response_plain(self, openai_cog): + """Plain Gemini call has no tools and uses plain instructions.""" + mock_response = MagicMock(text="gemini reply ") + openai_cog._gemini_client = MagicMock() + openai_cog._gemini_client.aio.models.generate_content = AsyncMock(return_value=mock_response) + + result = await openai_cog._get_gemini_response("hi", use_web=False) + + assert result == "gemini reply" + call_args = openai_cog._gemini_client.aio.models.generate_content.call_args[1] + assert call_args["model"] == "gemini-test" + assert call_args["contents"] == "hi" + # tools is absent in plain mode + assert "tools" not in call_args["config"].model_dump(exclude_none=True) or call_args["config"].tools is None + + @pytest.mark.asyncio + async def test_get_gemini_response_web_adds_google_search_tool(self, openai_cog): + """use_web=True attaches a GoogleSearch tool in the config.""" + mock_response = MagicMock(text="ok") + openai_cog._gemini_client = MagicMock() + openai_cog._gemini_client.aio.models.generate_content = AsyncMock(return_value=mock_response) + + await openai_cog._get_gemini_response("hi", use_web=True) + + call_args = openai_cog._gemini_client.aio.models.generate_content.call_args[1] + # The config carries a tool that has a non-None google_search attribute. + assert call_args["config"].tools is not None + assert any(getattr(t, "google_search", None) is not None for t in call_args["config"].tools) + @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") @patch("src.bot.cogs.open_ai.bot_utils.send_embed") @@ -146,8 +337,8 @@ async def test_ai_command_error(self, mock_send_embed, mock_get_settings, openai """Test AI command with OpenAI API error.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, "_get_ai_response", side_effect=Exception("API Error")): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="What is Python?") + with patch.object(openai_cog, "_get_openai_response", side_effect=Exception("API Error")): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="What is Python?") mock_ctx.send.assert_called_once() # progress message was sent mock_send_embed.assert_called_once() @@ -163,10 +354,10 @@ async def test_ai_command_error(self, mock_send_embed, mock_get_settings, openai @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_success( + async def test_get_openai_response_success( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): - """Test successful _get_ai_response method.""" + """Test successful _get_openai_response method.""" mock_get_settings.return_value = mock_bot_settings # Mock the client instance directly @@ -174,7 +365,7 @@ async def test_get_ai_response_success( mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - result = await openai_cog._get_ai_response("What is Python?", use_web=False) + result = await openai_cog._get_openai_response("What is Python?", use_web=False) assert result == "This is a mock AI response from OpenAI." @@ -192,7 +383,7 @@ async def test_get_ai_response_success( @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_web_success( + async def test_get_openai_response_web_success( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): """Web variant: uses web-grounded instructions and the web_search tool.""" @@ -202,7 +393,7 @@ async def test_get_ai_response_web_success( mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - result = await openai_cog._get_ai_response("What is Python?", use_web=True) + result = await openai_cog._get_openai_response("What is Python?", use_web=True) assert result == "This is a mock AI response from OpenAI." call_args = mock_client.responses.create.call_args @@ -212,10 +403,10 @@ async def test_get_ai_response_web_success( @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_with_leading_trailing_spaces( + async def test_get_openai_response_with_leading_trailing_spaces( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): - """Test _get_ai_response strips leading/trailing spaces.""" + """Test _get_openai_response strips leading/trailing spaces.""" mock_get_settings.return_value = mock_bot_settings mock_openai_response.output_text = " Response with spaces " @@ -224,7 +415,7 @@ async def test_get_ai_response_with_leading_trailing_spaces( mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - result = await openai_cog._get_ai_response("Test message", use_web=False) + result = await openai_cog._get_openai_response("Test message", use_web=False) assert result == "Response with spaces" @@ -301,7 +492,7 @@ async def test_ai_command_with_different_models(self, mock_send_embed, openai_co mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Test question") + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="Test question") # Verify correct model was used call_args = mock_client.responses.create.call_args @@ -317,8 +508,8 @@ async def test_ai_command_with_long_question( mock_get_settings.return_value = mock_bot_settings long_question = "What is " + "very " * 1000 + "long question?" - with patch.object(openai_cog, "_get_ai_response", return_value="Short answer"): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text=long_question) + with patch.object(openai_cog, "_get_openai_response", return_value="Short answer"): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text=long_question) mock_send_embed.assert_called_once() embed = mock_send_embed.call_args[0][1] @@ -334,8 +525,8 @@ async def test_ai_command_with_special_characters( mock_get_settings.return_value = mock_bot_settings special_question = "What is 2+2? 🤔 And émojis & spéciál chars?" - with patch.object(openai_cog, "_get_ai_response", return_value="4! 😊"): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text=special_question) + with patch.object(openai_cog, "_get_openai_response", return_value="4! 😊"): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text=special_question) mock_send_embed.assert_called_once() embed = mock_send_embed.call_args[0][1] @@ -343,7 +534,7 @@ async def test_ai_command_with_special_characters( @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_system_message_content( + async def test_get_openai_response_system_message_content( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): """Test that system message has correct content.""" @@ -354,7 +545,7 @@ async def test_get_ai_response_system_message_content( mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - await openai_cog._get_ai_response("Test message", use_web=False) + await openai_cog._get_openai_response("Test message", use_web=False) call_args = mock_client.responses.create.call_args[1] assert call_args["instructions"] == openai_cog._instructions @@ -362,7 +553,7 @@ async def test_get_ai_response_system_message_content( @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_api_parameters( + async def test_get_openai_response_api_parameters( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): """Test that OpenAI API is called with correct parameters.""" @@ -373,7 +564,7 @@ async def test_get_ai_response_api_parameters( mock_client.responses.create = AsyncMock(return_value=mock_openai_response) openai_cog._openai_client = mock_client - await openai_cog._get_ai_response("Test message", use_web=False) + await openai_cog._get_openai_response("Test message", use_web=False) call_args = mock_client.responses.create.call_args[1] assert call_args["max_output_tokens"] is None @@ -432,13 +623,13 @@ async def test_ai_command_error_logging( mock_get_settings.return_value = mock_bot_settings test_error = Exception("Test API Error") - with patch.object(openai_cog, "_get_ai_response", side_effect=test_error): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Test question") + with patch.object(openai_cog, "_get_openai_response", side_effect=test_error): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="Test question") # Verify error was logged with correct message openai_cog.bot.log.error.assert_called_once() log_call = openai_cog.bot.log.error.call_args[0][0] - assert "OpenAI API error:" in log_call + assert "API error:" in log_call # provider-prefixed assert "Test API Error" in log_call @pytest.mark.asyncio @@ -450,8 +641,8 @@ async def test_ai_command_send_embed_parameters( """Test that send_embed is called with correct parameters.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, "_get_ai_response", return_value="Test response"): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Test question") + with patch.object(openai_cog, "_get_openai_response", return_value="Test response"): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="Test question") # Verify send_embed was called with ctx, embed, and False mock_send_embed.assert_called_once() @@ -462,8 +653,8 @@ async def test_ai_command_send_embed_parameters( @pytest.mark.asyncio @patch("src.bot.cogs.open_ai.get_bot_settings") - async def test_get_ai_response_empty_response(self, mock_get_settings, openai_cog, mock_bot_settings): - """Test _get_ai_response with empty response from OpenAI.""" + async def test_get_openai_response_empty_response(self, mock_get_settings, openai_cog, mock_bot_settings): + """Test _get_openai_response with empty response from OpenAI.""" mock_get_settings.return_value = mock_bot_settings # Mock empty response (whitespace only) @@ -475,7 +666,7 @@ async def test_get_ai_response_empty_response(self, mock_get_settings, openai_co mock_client.responses.create = AsyncMock(return_value=mock_response) openai_cog._openai_client = mock_client - result = await openai_cog._get_ai_response("Test message", use_web=False) + result = await openai_cog._get_openai_response("Test message", use_web=False) assert result == "" # Should strip to empty string @@ -528,8 +719,8 @@ async def test_ai_command_pagination( mock_dal_class.return_value = mock_dal long_response = "a" * 3000 - with patch.object(openai_cog, "_get_ai_response", return_value=long_response): - await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Long question") + with patch.object(openai_cog, "_get_openai_response", return_value=long_response): + await openai_cog.gpt.callback(openai_cog, mock_ctx, msg_text="Long question") # ctx.send called twice: progress message, then the paginated first page assert mock_ctx.send.call_count == 2 diff --git a/utilities/start.sh b/utilities/start.sh index 7a728e0..293b3be 100644 --- a/utilities/start.sh +++ b/utilities/start.sh @@ -8,7 +8,7 @@ docker compose down docker images 'discordbot*' -a -q | xargs -r docker rmi -f # ensure logs dir is writable by container's botuser (uid 1000) -sudo chown -R 1000:1000 "$PROJECT_DIR/logs" +sudo chown -RH 1000:1000 "$PROJECT_DIR/logs" sudo chmod 755 "$PROJECT_DIR/logs" docker compose up -d --build --force-recreate diff --git a/uv.lock b/uv.lock index 8b4caed..dcbd81a 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,42 +24,49 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" }, + { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" }, + { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" }, + { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" }, + { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" }, + { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" }, ] [[package]] @@ -97,6 +104,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.105.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/46/47581b8c689c743ceabf6a0f9ff48472160900ce802d26c0fb50423997b3/anthropic-0.105.2.tar.gz", hash = "sha256:0e26b90841c2dced7cc6e98d21d5517d0be33f1876b8e779f478202e28bcaa07", size = 853789, upload-time = "2026-05-29T00:21:14.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/75/be0c357e33a5a56c8f9db5b4212f886138d2bf59c0952d858f6b75d710ef/anthropic-0.105.2-py3-none-any.whl", hash = "sha256:e53ed5f6bf36fb1ecb9b25d8634cfd30e02fab9fb3374a0c2d5c585874757230", size = 837507, upload-time = "2026-05-29T00:21:15.528Z" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -347,6 +373,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + [[package]] name = "ddcdatabases" version = "4.0.1" @@ -381,14 +460,16 @@ wheels = [ [[package]] name = "discordbot" -version = "3.0.16" +version = "3.0.17" source = { virtual = "." } dependencies = [ { name = "alembic" }, + { name = "anthropic" }, { name = "beautifulsoup4" }, { name = "better-profanity" }, { name = "ddcdatabases", extra = ["postgres"] }, { name = "discord-py" }, + { name = "google-genai" }, { name = "gtts" }, { name = "openai" }, { name = "pynacl" }, @@ -408,10 +489,12 @@ dev = [ [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.18.4" }, + { name = "anthropic", specifier = ">=0.105.2" }, { name = "beautifulsoup4", specifier = ">=4.14.3" }, { name = "better-profanity", specifier = ">=0.7.0" }, { name = "ddcdatabases", extras = ["postgres"], specifier = ">=4.0.1" }, { name = "discord-py", specifier = ">=2.7.1" }, + { name = "google-genai", specifier = ">=2.7.0" }, { name = "gtts", specifier = ">=2.5.4" }, { name = "openai", specifier = ">=2.38.0" }, { name = "pynacl", specifier = ">=1.6.2" }, @@ -451,6 +534,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -492,6 +584,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/7b/6eb3b3d545b6bb4c374acba1ccf91b0f33b605e551536a6243cfcef2f07f/google_genai-2.7.0.tar.gz", hash = "sha256:3c6f32f5ced9877ededd1b384b5e5b7f09c20046ec3390b662b16d8cd1882ac5", size = 555853, upload-time = "2026-05-28T15:39:24.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/7a8be39e9d698e80e9db796514efbc6083dbd787bdb9a101e8ba47248e5e/google_genai-2.7.0-py3-none-any.whl", hash = "sha256:21cac381e09a869151706aba797b6a4f96cfe92c484e13204d092caee7ff11cb", size = 822545, upload-time = "2026-05-28T15:39:22.907Z" }, +] + [[package]] name = "greenlet" version = "3.5.1" @@ -731,7 +862,7 @@ wheels = [ [[package]] name = "openai" -version = "2.38.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -743,9 +874,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/6c/3fc80e2a9a6a221bea772e8851912e4d86203ee66a73f17639a9cc9bfbb8/openai-2.39.0.tar.gz", hash = "sha256:8d915a72448314c97c719fa63e3c88efa6041e806c546064aaf1edc5e6fffc45", size = 774874, upload-time = "2026-06-01T18:58:18.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, + { url = "https://files.pythonhosted.org/packages/02/5b/7c759d211cba60b61136556b589afeaa4087d07a6ddfcff9f5f0b1ef5121/openai-2.39.0-py3-none-any.whl", hash = "sha256:be9de9b3a1f264777c27d3efb266b5abd37f3d25b48f8d0ac1c4db14e5c73bfd", size = 1347460, upload-time = "2026-06-01T18:58:16.764Z" }, ] [[package]] @@ -866,6 +997,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1164,6 +1316,15 @@ asyncio = [ { name = "greenlet" }, ] +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + [[package]] name = "testcontainers" version = "4.15.0rc2" @@ -1267,6 +1428,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "wrapt" version = "2.2.1"