# AGENTS.md - Discord Bot v2.0 Guidelines for AI coding agents working in this repository. ## Quick Reference **Start bot**: `python bot.py` **Run all tests**: `python -m pytest --tb=short -q` **Run single test file**: `python -m pytest tests/test_models.py -v` **Run single test**: `python -m pytest tests/test_models.py::TestTeamModel::test_team_creation_minimal -v` **Run tests matching pattern**: `python -m pytest -k "test_player" -v` ## Project Structure - `bot.py` - Main entry point - `commands/` - Discord slash commands (package-based) - `services/` - API service layer (BaseService pattern) - `models/` - Pydantic data models - `views/` - Discord UI components (embeds, modals) - `utils/` - Logging, decorators, caching - `tests/` - pytest test suite ## Code Style ### Imports Order: stdlib, third-party, local. Separate groups with blank lines. ```python import asyncio from typing import Optional, List import discord from discord.ext import commands from services.player_service import player_service from utils.decorators import logged_command ``` ### Formatting - Line length: 100 characters max - Docstrings: Google style with triple quotes - Indentation: 4 spaces - Trailing commas in multi-line structures ### Type Hints Always use type hints for function signatures: ```python async def get_player(self, player_id: int) -> Optional[Player]: async def search_players(self, query: str, limit: int = 10) -> List[Player]: ``` ### Naming Conventions - Classes: `PascalCase` (PlayerService, TeamInfoCommands) - Functions/methods: `snake_case` (get_player, search_players) - Constants: `UPPER_SNAKE_CASE` (SBA_CURRENT_SEASON) - Private: prefix with `_` (_client, _team_service) ### Error Handling Use custom exceptions from `exceptions.py`. Prefer "raise or return" over Optional: ```python from exceptions import APIException, PlayerNotFoundError async def get_player(self, player_id: int) -> Player: result = await self.get_by_id(player_id) if result is None: raise PlayerNotFoundError(f"Player {player_id} not found") return result ``` ## Discord Command Patterns ### Always use @logged_command decorator Eliminates boilerplate logging. Class must have `self.logger` attribute: ```python class PlayerInfoCommands(commands.Cog): def __init__(self, bot): self.bot = bot self.logger = get_contextual_logger(f'{__name__}.PlayerInfoCommands') @discord.app_commands.command(name="player") @logged_command("/player") async def player_command(self, interaction, name: str): # Business logic only - no try/catch boilerplate needed player = await player_service.get_player_by_name(name) await interaction.followup.send(embed=create_embed(player)) ``` ### Autocomplete: Use standalone functions (not methods) ```python async def player_name_autocomplete( interaction: discord.Interaction, current: str, ) -> List[discord.app_commands.Choice[str]]: if len(current) < 2: return [] try: players = await player_service.search_players(current, limit=25) return [discord.app_commands.Choice(name=p.name, value=p.name) for p in players] except Exception: return [] # Never break autocomplete class MyCommands(commands.Cog): @discord.app_commands.command() @discord.app_commands.autocomplete(name=player_name_autocomplete) async def my_command(self, interaction, name: str): ... ``` ### Embed emoji rules Template methods auto-add emojis. Never double up: ```python # CORRECT - template adds emoji embed = EmbedTemplate.success(title="Operation Completed") # Results in: "Operation Completed" # WRONG - double emoji embed = EmbedTemplate.success(title="Operation Completed") # Results in: " Operation Completed" # For custom emoji, use create_base_embed embed = EmbedTemplate.create_base_embed(title="Custom Title", color=EmbedColors.SUCCESS) ``` ## Service Layer ### Never bypass services for API calls ```python # CORRECT player = await player_service.get_player(player_id) # WRONG - never do this client = await player_service.get_client() await client.get(f'players/{player_id}') ``` ### Key service methods - `TeamService.get_team(team_id)` - not `get_team_by_id()` - `PlayerService.search_players(query, limit, all_seasons=True)` - cross-season search ## Models ### Use from_api_data() classmethod ```python player = Player.from_api_data(api_response) ``` ### Database entities require id field ```python class Player(SBABaseModel): id: int = Field(..., description="Player ID from database") # Required, not Optional ``` ## Testing ### Use aioresponses for HTTP mocking ```python from aioresponses import aioresponses @pytest.mark.asyncio async def test_get_player(): with aioresponses() as m: m.get("https://api.example.com/v3/players/1", payload={"id": 1, "name": "Test"}) result = await api_client.get("players", object_id=1) assert result["name"] == "Test" ``` ### Provide complete model data Pydantic validates all fields. Use helper functions for test data: ```python def create_player_data(player_id: int, name: str, **kwargs): return {"id": player_id, "name": name, "wara": 2.5, "season": 13, "pos_1": "CF", **kwargs} ``` ## Critical Rules 1. **Git**: Never commit directly to `main`. Create feature branches. 2. **Services**: Always use service layer methods, never direct API client access. 3. **Embeds**: Don't add emojis to titles when using template methods (success/error/warning/info). 4. **Tests**: Include docstrings explaining "what" and "why" for each test. 5. **Commits**: Do not commit without user approval. ## Documentation Check `CLAUDE.md` files in directories for detailed patterns: - `commands/CLAUDE.md` - Command architecture - `services/CLAUDE.md` - Service patterns - `models/CLAUDE.md` - Model validation - `tests/CLAUDE.md` - Testing strategies