Comprehensive guide covering: - Build/lint/test commands including single test execution - Code style: imports, formatting, types, naming, error handling - Discord patterns: @logged_command, autocomplete, embed emojis - Service layer abstraction rules - Model patterns (from_api_data, required IDs) - Testing with aioresponses and complete model data - Critical rules for git, services, and commits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
5.8 KiB
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 pointcommands/- Discord slash commands (package-based)services/- API service layer (BaseService pattern)models/- Pydantic data modelsviews/- Discord UI components (embeds, modals)utils/- Logging, decorators, cachingtests/- pytest test suite
Code Style
Imports
Order: stdlib, third-party, local. Separate groups with blank lines.
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:
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:
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:
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)
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:
# 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
# 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)- notget_team_by_id()PlayerService.search_players(query, limit, all_seasons=True)- cross-season search
Models
Use from_api_data() classmethod
player = Player.from_api_data(api_response)
Database entities require id field
class Player(SBABaseModel):
id: int = Field(..., description="Player ID from database") # Required, not Optional
Testing
Use aioresponses for HTTP mocking
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:
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
- Git: Never commit directly to
main. Create feature branches. - Services: Always use service layer methods, never direct API client access.
- Embeds: Don't add emojis to titles when using template methods (success/error/warning/info).
- Tests: Include docstrings explaining "what" and "why" for each test.
- Commits: Do not commit without user approval.
Documentation
Check CLAUDE.md files in directories for detailed patterns:
commands/CLAUDE.md- Command architectureservices/CLAUDE.md- Service patternsmodels/CLAUDE.md- Model validationtests/CLAUDE.md- Testing strategies