{"id":51571340,"url":"https://github.com/ruidosujeira/discordkit","last_synced_at":"2026-07-10T19:32:22.831Z","repository":{"id":364867942,"uuid":"1268841301","full_name":"ruidosujeira/discordkit","owner":"ruidosujeira","description":"A modern, type-safe Python Discord framework focused on excellent developer experience and clean architecture.","archived":false,"fork":false,"pushed_at":"2026-06-23T16:23:09.000Z","size":143,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T18:22:04.409Z","etag":null,"topics":["asyncio","bot-framework","discord","discord-api","discord-bot","framework","mit-license","open-source","pydantic","python","python3","slash-commands","typed"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruidosujeira.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T02:06:56.000Z","updated_at":"2026-06-23T16:24:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ruidosujeira/discordkit","commit_stats":null,"previous_names":["ruidosujeira/discordkit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ruidosujeira/discordkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruidosujeira%2Fdiscordkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruidosujeira%2Fdiscordkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruidosujeira%2Fdiscordkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruidosujeira%2Fdiscordkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruidosujeira","download_url":"https://codeload.github.com/ruidosujeira/discordkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruidosujeira%2Fdiscordkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35341768,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","bot-framework","discord","discord-api","discord-bot","framework","mit-license","open-source","pydantic","python","python3","slash-commands","typed"],"created_at":"2026-07-10T19:32:22.750Z","updated_at":"2026-07-10T19:32:22.816Z","avatar_url":"https://github.com/ruidosujeira.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DiscordKit\n\n[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python\u0026logoColor=white)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![CI](https://github.com/ruidosujeira/discordkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ruidosujeira/discordkit/actions/workflows/ci.yml)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![Mypy](https://img.shields.io/badge/mypy-strict-2a6db2?logo=python)](https://mypy-lang.org/)\n\n**A modern, strongly-typed Python framework for Discord bots with excellent Developer Experience.**\n\nDiscordKit is a fresh, from-the-ground-up framework focused on **clarity, type safety, and joy of use** — without the legacy baggage or heavy magic of older libraries.\n\nIt is designed to be the framework you actually *want* to use for both small personal bots and large, long-running production bots.\n\n---\n\n## Table of Contents\n\n- [Why DiscordKit?](#why-discordkit)\n- [Installation](#installation)\n- [Quickstart](#quickstart)\n- [Core Concepts](#core-concepts)\n  - [Client](#client)\n  - [Commands \u0026 Options](#commands--options)\n  - [Subcommands \u0026 Groups](#subcommands--groups)\n  - [Components \u0026 Interactions](#components--interactions)\n  - [Cache](#cache)\n- [Using the Cache](#using-the-cache)\n- [Rate Limiting](#rate-limiting)\n- [Error Handling](#error-handling)\n- [Production Considerations](#production-considerations)\n- [Extending DiscordKit](#extending-discordkit)\n- [API Reference](#api-reference)\n- [Hot Reload](#hot-reload-development)\n- [Testing](#testing-your-bot)\n- [Examples](#examples)\n- [Contributing](#contributing)\n- [Roadmap \u0026 Philosophy](#roadmap--philosophy)\n- [License](#license)\n\n---\n\n## Why DiscordKit?\n\n- **Strong typing everywhere** — Powered by Pydantic v2 + `Annotated` + `Option`. Your IDE actually helps you.\n- **Beautiful, explicit API** — Minimal magic. You can read the code and understand what happens.\n- **First-class modern Discord features** — Subcommands \u0026 nested groups, rich options, components, modals, and autocomplete done right.\n- **Production ready** — Robust error handling, structured logging, intelligent rate limiting, and a mature cache system.\n- **Outstanding DX** — `discordkit new`, `discordkit run` (hot reload), and clean separation of concerns.\n- **Grows with you** — Starts simple. Scales cleanly to complex command hierarchies and large codebases.\n\nIf you are tired of fighting your framework or writing `if isinstance` everywhere, DiscordKit is for you.\n\n---\n\n## Installation\n\n**Recommended (uv):**\n\n```bash\nuv add discordkit\n```\n\n**With pip:**\n\n```bash\npip install discordkit\n```\n\nPython **3.12+** is required.\n\n---\n\n## Quickstart\n\n```python\nimport os\nfrom typing import Annotated\n\nfrom discordkit import Client, Config, User\nfrom discordkit.commands import Option, command\nfrom discordkit.types import Intents\n\nconfig = Config(token=os.environ[\"DISCORD_TOKEN\"], intents=Intents.DEFAULT)\nbot = Client(config)\n\n@command(name=\"ping\", description=\"Replies with Pong!\")\nasync def ping(ctx):\n    await ctx.respond(\"Pong! 🏓\")\n\n@command(name=\"greet\", description=\"Greet someone\")\nasync def greet(\n    ctx,\n    user: Annotated[User, Option(\"Who to greet\")],\n    message: Annotated[str, Option(\"Optional message\", max_length=100)] = \"Hello!\",\n):\n    await ctx.respond(f\"{message} {user.mention}\")\n\n@bot.event(\"ready\")\nasync def on_ready(ctx):\n    print(f\"✅ Logged in as {bot.user}\")\n\nbot.run()\n```\n\nRun with hot reload during development:\n\n```bash\ndiscordkit run\n# or\ndiscordkit run your_bot.py\n```\n\n---\n\n## Core Concepts\n\n### Client\n\nThe `Client` is the heart of every DiscordKit bot. It wires together:\n\n| Subsystem | Responsibility |\n|-----------|----------------|\n| `bot.http` | REST API calls with automatic rate-limit handling |\n| `bot.gateway` | WebSocket connection and event dispatch |\n| `bot.commands` | Slash command registry and routing |\n| `bot.components` | Button, select, and modal routing |\n| `bot.cache` | Typed in-memory cache (swappable via `CacheBackend`) |\n\n```python\nfrom discordkit import Client, Config\n\nbot = Client(Config(token=\"...\", intents=Intents.DEFAULT))\n\n# Lifecycle\nawait bot.start()   # async entry\nbot.run()           # blocking helper (most common)\n\n# Readiness\nbot.is_ready        # True after READY event\nbot.user            # Bot User model\nbot.application_id  # Application ID for command sync\n```\n\nThe client automatically syncs global slash commands on `READY` and populates the cache from resolved command options when `bot.auto_cache` is enabled (default).\n\n### Commands \u0026 Options\n\nDiscordKit uses a powerful `Annotated[T, Option(...)]` system:\n\n```python\nfrom typing import Annotated\nfrom discordkit.commands import Option\nfrom discordkit.models import User, Role, Channel\n\n@command(name=\"ban\", description=\"Ban a user\")\nasync def ban(\n    ctx,\n    user: Annotated[User, Option(\"User to ban\")],\n    reason: Annotated[str, Option(\"Reason\", min_length=5, max_length=400)],\n    days: Annotated[int, Option(\"Days of messages to delete\", min_value=0, max_value=7)] = 0,\n):\n    ...\n```\n\nFull feature set:\n- `description`, `required` (auto-inferred from defaults)\n- `min_length` / `max_length`, `min_value` / `max_value`\n- `choices`, `channel_types`, `autocomplete`\n- Rich Discord types: `User`, `Member`, `Role`, `Channel`, `Attachment`\n\nWhen a slash command runs, `resolve_options` turns raw Discord payloads into real Pydantic models — so `ctx.options[\"user\"]` is a `User`, not a raw ID.\n\n### Subcommands \u0026 Groups\n\n```python\n@bot.group(name=\"config\", description=\"Server settings\")\nasync def config(ctx): ...\n\n@config.command(name=\"set\", description=\"Change a setting\")\nasync def config_set(ctx, key: str, value: str): ...\n\n# Nested groups are supported\n@config.group(name=\"advanced\", description=\"Dangerous options\")\nasync def advanced(ctx): ...\n\n@advanced.command(name=\"reset\", description=\"Factory reset\")\nasync def advanced_reset(ctx, confirm: bool = False): ...\n```\n\nThe framework builds the correct Discord payload, routes to the exact leaf handler, and resolves options for deeply nested commands.\n\n### Components \u0026 Interactions\n\n```python\nfrom discordkit.interactions import Button\nfrom discordkit.types import ButtonStyle\nfrom discordkit.components import ButtonContext\n\n@bot.component(\"confirm\")\nasync def on_confirm(ctx: ButtonContext):\n    await ctx.edit_message(content=\"Confirmed!\", components=[])\n\n# In a command:\nconfirm_btn = Button(label=\"Confirm\", style=ButtonStyle.SUCCESS, custom_id=\"confirm\")\nawait ctx.respond(\"Are you sure?\", components=[confirm_btn.to_dict()])\n```\n\nModals and selects work the same way with `@bot.modal(\"id\")` and `@bot.component(\"prefix:\")` for prefix matching.\n\n### Cache\n\nDiscordKit ships a production-grade cache built on a stable `CacheBackend` interface:\n\n- **Typed stores** for `User`, `Member`, `Guild`, and `Channel`\n- **TTL** with global defaults, per-entity overrides, and sliding expiration (`touch_on_read`)\n- **LRU eviction** when `max_size` is reached\n- **`get_or_fetch`** — the cache-aside pattern bots use every day\n- **Statistics** — hits, misses, evictions, hit rate\n- **Thread-safe** `MemoryCache` for single-process bots\n- **Persistent** — `PersistentCache` stores entries in SQLite across restarts\n- **Extensible** — implement `CacheBackend` for Redis or other backends\n\nSee [Using the Cache](#using-the-cache) for detailed examples and best practices.\n\n---\n\n## Using the Cache\n\n### Automatic population\n\nWhen users run slash commands, resolved options are stored automatically:\n\n```python\n@command(name=\"profile\", description=\"Show a user profile\")\nasync def profile(ctx, user: Annotated[User, Option(\"User\")]):\n    # `user` came from Discord AND was cached\n    cached = bot.get_cached_user(user.id)  # same object, no API call\n    await ctx.respond(f\"Hello, {cached.display_name}!\")\n```\n\nDisable auto-caching if you prefer full manual control:\n\n```python\nbot.auto_cache = False\n```\n\n### Configuration\n\nConfigure the cache at startup with sensible production defaults:\n\n```python\nbot.configure_cache(\n    default_ttl=600,          # 10 minutes fallback\n    max_size=20_000,        # LRU eviction kicks in above this\n    touch_on_read=True,     # renew TTL on every read (sliding window)\n    ttl_by_type={\n        \"user\": 900,        # users change infrequently\n        \"member\": 180,      # nicknames/roles change more often\n        \"guild\": 1800,\n        \"channel\": 300,\n    },\n)\n```\n\nOr construct a `MemoryCache` directly and assign it:\n\n```python\nfrom discordkit import MemoryCache, EvictionPolicy\n\nbot.cache = MemoryCache(\n    default_ttl=300,\n    max_size=10_000,\n    eviction_policy=EvictionPolicy.LRU,\n    touch_on_read=True,\n)\n```\n\n### Manual read/write\n\n```python\n# Store\nbot.cache.set_user(user)\nbot.cache.set_member(member, guild_id=ctx.guild_id)\nbot.cache.set_guild(guild)\nbot.cache.set_channel(channel)\n\n# Read\nuser = bot.cache.get_user(user_id)\nmember = bot.cache.get_member(guild_id, user_id)   # guild-scoped\nguild = bot.cache.get_guild(guild_id)\n\n# Invalidate\nbot.cache.invalidate_user(user_id)\nbot.cache.invalidate_member(guild_id, user_id)\nbot.invalidate_guild_cache(guild_id)   # all members in a guild\nbot.cache.invalidate_by_type(\"user\")   # bulk clear\nbot.cache.clear()                      # wipe everything\n\n# Renew TTL without re-fetching\nbot.cache.touch_user(user_id)\nbot.cache.touch_member(guild_id, user_id)\n```\n\n### Cache-aside with `get_or_fetch`\n\nThe most common pattern in real bots — check cache first, fetch on miss:\n\n```python\n@command(name=\"lookup\", description=\"Look up a user by ID\")\nasync def lookup(ctx, user_id: Annotated[int, Option(\"User ID\")]):\n    async def fetch_from_api():\n        data = await bot.http.get_user(user_id)\n        return User.model_validate(data)\n\n    user = await bot.cache.get_or_fetch_user(user_id, fetch_from_api)\n    # Or via client convenience:\n    # user = await bot.fetch_user_cached(user_id, fetch_from_api)\n\n    if user is None:\n        await ctx.respond(\"User not found.\", ephemeral=True)\n        return\n    await ctx.respond(f\"Found: **{user.display_name}**\")\n```\n\n`get_or_fetch` accepts both sync and async fetchers. The same pattern exists for members, guilds, and channels.\n\n### Statistics and monitoring\n\n```python\nstats = bot.cache_stats()\nprint(f\"Hit rate: {stats.hit_rate:.1%}\")\nprint(f\"Entries: {stats.total_size} (users={stats.size_users}, members={stats.size_members})\")\nprint(f\"Evictions: {stats.evictions}\")\n```\n\nUse these metrics in production to tune TTLs and `max_size`.\n\n### Persistent cache (SQLite)\n\nFor bots that restart frequently, use `PersistentCache` to avoid cold-cache API spikes:\n\n```python\nfrom discordkit import PersistentCache\n\n# Direct usage\ncache = PersistentCache(path=\".data/discordkit_cache.db\", max_size=20_000)\nbot.cache = cache\n\n# Or via Client helper\nbot.configure_cache(\n    persistent=True,\n    cache_path=\".data/discordkit_cache.db\",\n    default_ttl=600,\n    max_size=20_000,\n)\n```\n\nOn startup, non-expired entries are loaded from disk. Writes and invalidations are mirrored to SQLite automatically (stdlib `sqlite3`, no extra dependencies).\n\n```python\n# Maintenance\nbot.cache.vacuum()   # purge expired rows, reclaim disk space\nprint(bot.cache.db_path)\n```\n\n### Best practices\n\n1. **Let auto-cache do the easy work** — slash command options are free cache hits.\n2. **Use `get_or_fetch` for API calls** — never call the REST API without checking cache first.\n3. **Invalidate on mutations** — after banning, kicking, or updating roles, call `invalidate_member`.\n4. **Tune TTLs per entity** — members change more often than guild metadata.\n5. **Set `max_size`** — prevents unbounded memory growth in large bots.\n6. **Plan for Redis** — implement `CacheBackend` when you need multi-process or persistence.\n\n---\n\n## Rate Limiting\n\nDiscordKit handles Discord rate limits transparently inside `DiscordHTTPClient`. You normally never think about it — but understanding the internals helps when debugging slow responses.\n\n### How it works\n\n1. **Pre-request check** — Before each REST call, `RateLimiter.acquire()` checks whether the target bucket (or a global limit) is exhausted. If so, it sleeps until the reset time.\n2. **Header parsing** — Every response updates state from Discord headers:\n   - `X-RateLimit-Limit`\n   - `X-RateLimit-Remaining`\n   - `X-RateLimit-Reset` / `X-RateLimit-Reset-After`\n   - `X-RateLimit-Bucket`\n   - `X-RateLimit-Global`\n3. **429 handling** — On `429 Too Many Requests`, the client reads `retry_after` from the body, sleeps, and retries (up to a safe limit).\n4. **Logging** — Rate-limit waits are logged at `INFO`/`WARNING` so you can spot hot endpoints in production.\n\n```python\n# You don't need to write this — it happens automatically:\n# await rate_limiter.acquire(bucket)\n# response = await session.request(...)\n# rate_limiter.update(response.headers, response.status)\n```\n\n### What you should do\n\n- **Use the cache** — fewer API calls means fewer rate-limit hits.\n- **Batch when possible** — `bulk_get_users` and cache-aside reduce round-trips.\n- **Watch your logs** — repeated `Rate limit for bucket` messages indicate a hot loop.\n- **Defer long commands** — for work that takes \u003e3 seconds, call `ctx.defer()` before heavy API usage.\n\n---\n\n## Error Handling\n\nDiscordKit catches errors inside command, component, autocomplete, and event handlers so your bot stays alive. Register a global handler for observability:\n\n```python\nimport logging\n\nlogger = logging.getLogger(\"mybot\")\n\n@bot.error_handler\nasync def on_error(error: Exception, context: dict):\n    \"\"\"\n    context keys:\n      - type:     \"command\" | \"component\" | \"autocomplete\"\n      - command:  full command path (for commands)\n      - user_id:  Discord user ID\n      - guild_id: guild ID (if applicable)\n      - channel_id: channel ID (if applicable)\n    \"\"\"\n    logger.error(\n        \"Handler failed | type=%s | cmd=%s | user=%s | error=%s\",\n        context.get(\"type\"),\n        context.get(\"command\"),\n        context.get(\"user_id\"),\n        error,\n        exc_info=True,\n    )\n\n    # Production: send to Sentry, a logging channel, etc.\n    # await notify_sentry(error, context)\n```\n\n### What happens on failure\n\n| Handler type | User sees | Bot state |\n|-------------|-----------|-----------|\n| Slash command | Ephemeral \"unexpected error\" message | Keeps running |\n| Component/modal | Logged, no crash | Keeps running |\n| Autocomplete | Empty suggestions (safe fallback) | Keeps running |\n| Event handler | Logged exception | Keeps running |\n\n### Local recovery\n\nHandle expected errors inside your command and only escalate what you cannot recover:\n\n```python\n@command(name=\"divide\", description=\"Divide two numbers\")\nasync def divide(ctx, a: int, b: int):\n    if b == 0:\n        await ctx.respond(\"Cannot divide by zero.\", ephemeral=True)\n        return\n    await ctx.respond(f\"Result: {a / b}\")\n\n@command(name=\"risky\", description=\"May fail internally\")\nasync def risky(ctx):\n    try:\n        result = await do_something()\n    except ValueError as e:\n        await ctx.respond(f\"Handled: {e}\", ephemeral=True)\n        return\n    await ctx.respond(f\"Done: {result}\")\n```\n\nSee `examples/error_handling.py` for a complete production-oriented demo.\n\n---\n\n## Production Considerations\n\n### Always register an error handler\n\nAt minimum, log structured context for every unhandled exception. In production, forward to Sentry, Datadog, or a private Discord logging channel.\n\n### Configure the cache deliberately\n\n```python\nbot.configure_cache(\n    default_ttl=600,\n    max_size=20_000,\n    touch_on_read=True,\n    ttl_by_type={\"member\": 120},\n)\n```\n\nMonitor `bot.cache_stats().hit_rate` and adjust TTLs based on your bot's access patterns.\n\n### Logging\n\n```python\nconfig = Config(\n    token=os.environ[\"DISCORD_TOKEN\"],\n    intents=Intents.DEFAULT,\n    log_level=\"INFO\",   # \"DEBUG\" only in development\n    debug=False,\n)\n```\n\n### Graceful shutdown\n\n`bot.run()` handles `KeyboardInterrupt`. For custom lifecycle:\n\n```python\nimport asyncio\n\nasync def main():\n    bot = Client(config)\n    try:\n        await bot.start()\n    finally:\n        await bot.close()\n\nasyncio.run(main())\n```\n\n### Security\n\n- Store tokens in environment variables, never in source code.\n- Use `SecretStr` in `Config` — tokens are not logged or repr'd.\n- Validate permissions in commands before performing destructive actions.\n\n### Performance checklist\n\n- [ ] Cache enabled with appropriate TTLs and `max_size`\n- [ ] `bot.auto_cache = True` (default)\n- [ ] Global error handler registered\n- [ ] `debug=False` in production\n- [ ] Long-running commands use `ctx.defer()`\n- [ ] Rate-limit warnings monitored in logs\n\n---\n\n## Extending DiscordKit\n\n### Custom cache backend (Redis, etc.)\n\nImplement `CacheBackend` and assign it to the client:\n\n```python\nfrom discordkit import CacheBackend\nfrom discordkit.models import User, Member, Guild, Channel\n\nclass RedisCache(CacheBackend):\n    def get_user(self, user_id: int) -\u003e User | None:\n        ...\n    def set_user(self, user: User, ttl: float | None = None) -\u003e None:\n        ...\n    # ... implement remaining abstract methods\n\nbot.configure_cache(backend=RedisCache(...))\n```\n\nThe interface is stable and documented — your backend only needs to satisfy the contract.\n\n### Custom commands and middleware patterns\n\nCommands are plain async functions registered via decorators:\n\n```python\nfrom discordkit.commands import command\n\n@command(name=\"hello\", description=\"Say hello\")\nasync def hello(ctx):\n    await ctx.respond(\"Hello!\")\n\nbot.add_command(hello)\n```\n\nFor cross-cutting concerns (auth, logging, cooldowns), wrap handlers:\n\n```python\nfrom functools import wraps\n\ndef requires_admin(func):\n    @wraps(func)\n    async def wrapper(ctx, *args, **kwargs):\n        if not ctx.member or not ctx.member_has_permission(\"administrator\"):\n            await ctx.respond(\"Admin only.\", ephemeral=True)\n            return\n        return await func(ctx, *args, **kwargs)\n    return wrapper\n\n@bot.command(name=\"purge\", description=\"Purge messages\")\n@requires_admin\nasync def purge(ctx, count: int):\n    ...\n```\n\n### Custom components\n\nRegister handlers with `@bot.component(custom_id)` or prefix patterns:\n\n```python\n@bot.component(\"ticket:\")\nasync def handle_ticket(ctx):\n    ticket_id = ctx.custom_id.split(\":\")[1]\n    ...\n```\n\n### Contributing new models\n\nModels live in `discordkit.models` and extend `DiscordModel` (Pydantic v2). Follow existing patterns in `user.py`, `guild.py`, etc.\n\n---\n\n## API Reference\n\nFull API documentation for every public class, method, and parameter:\n\n**[docs/API_REFERENCE.md](docs/API_REFERENCE.md)**\n\nCovers:\n- `Client` — lifecycle, events, commands, cache, error handling\n- `Config` — all configuration fields\n- `CacheBackend` / `MemoryCache` / `PersistentCache` — complete method tables\n- `Option` — constraints, validation rules, payload generation\n- Context types — `CommandContext`, `ButtonContext`, `ModalContext`, etc.\n- Models, types, rate limiting, and CLI\n\n---\n\n## Hot Reload (Development)\n\n```bash\n# From inside your project\ndiscordkit run\n```\n\nWatches your Python files and restarts the bot cleanly on save. Uses `watchfiles` under the hood.\n\n---\n\n## Testing Your Bot\n\nDiscordKit ships with a professional test setup using `pytest` + `pytest-asyncio`.\n\n```bash\n# Run tests\npytest\n\n# Or with coverage\npytest --cov=src/discordkit\n```\n\nKey test areas covered out of the box:\n- Command definition and payload generation (including subcommands)\n- Complex option resolution (`User`, `Role`, etc.)\n- Full slash command routing\n- Component handlers\n- Error handling paths\n- Cache (TTL, LRU, `get_or_fetch`, statistics)\n\nSee the `tests/` directory for examples of unit-testing commands without a real Discord connection.\n\n---\n\n## Examples\n\nHigh-quality, well-commented examples live in the `examples/` directory:\n\n| File | What it demonstrates |\n|------|---------------------|\n| `simple_bot.py` | Minimal getting-started bot |\n| `slash_commands.py` | Rich options + resolved models + autocomplete |\n| `subcommands.py` | Groups and nested groups |\n| `component_bot.py` | Buttons, selects, and modals |\n| `advanced_commands.py` | Heavy use of the `Option` system |\n| `moderation_bot.py` | Practical moderation with subcommands |\n| `ticket_system.py` | Buttons + modals + staff subcommands |\n| `error_handling.py` | Production error patterns |\n| `economy_bot.py` | Stateful bot patterns |\n| `suggestions_bot.py` | Community feature bot |\n\nStart with `simple_bot.py`, then explore `slash_commands.py` and `subcommands.py`.\n\n---\n\n## Roadmap \u0026 Philosophy\n\nDiscordKit prioritizes:\n\n1. **Developer happiness** and **type safety**\n2. **Correctness** over cleverness\n3. **Production stability**\n\nWe are actively improving:\n- Even better subcommand ergonomics and discovery\n- More first-class Discord models and helpers\n- Optional SQLAlchemy / async ORM integrations (via extensions)\n- Sharding helpers\n- Redis cache backend (as an official extension)\n\nContributions and feedback that align with the \"clean, typed, delightful\" philosophy are very welcome.\n\n---\n\n## Contributing\n\n### Local development setup (recommended)\n\nWe use [uv](https://docs.astral.sh/uv/) for fast dependency management.\n\n```bash\ngit clone https://github.com/discordkit/discordkit.git\ncd discordkit\n\n# Create virtual environment + install the project + dev dependencies\nuv sync --dev\n```\n\nActivate the environment when needed:\n\n```bash\nsource .venv/bin/activate   # or `uv run \u003ccommand\u003e` (no activation needed)\n```\n\n### Pre-commit hooks (strongly recommended)\n\nPre-commit runs fast checks (ruff, mypy, etc.) automatically before every commit.\n\n```bash\n# Install the git hooks (one-time setup)\nuv run pre-commit-install\n\n# Or directly:\nuv run pre-commit install\n```\n\nRun checks manually on the whole codebase:\n\n```bash\nuv run pre-commit run --all-files\n```\n\nThe configuration lives in [`.pre-commit-config.yaml`](.pre-commit-config.yaml). It includes:\n\n- `ruff` (lint + auto-fix)\n- `ruff-format`\n- `mypy` (uses your project's strict configuration from `pyproject.toml`)\n- Standard hygiene hooks (`check-yaml`, `trailing-whitespace`, `debug-statements`, etc.)\n\n### Running checks manually\n\n```bash\n# Lint\nuv run ruff check .\n\n# Format check\nuv run ruff format --check .\n\n# Type checking (strict)\nuv run mypy src/discordkit\n\n# Tests (with coverage)\nuv run test-cov\n\n# All of the above in one command\nuv run checks\n```\n\n### Continuous Integration\n\nEvery push and pull request to `main` runs the workflow defined in [`.github/workflows/ci.yml`](.github/workflows/ci.yml):\n\n- Matrix: Python 3.12 and 3.13\n- Steps: ruff check + format, mypy (strict), pytest + coverage\n- Uses `uv` + dependency caching for speed\n\nThe CI **must pass** before a PR can be merged.\n\n---\n\n## License\n\nMIT\n\n---\n\n**Made with care for developers who care about their tools.**","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruidosujeira%2Fdiscordkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruidosujeira%2Fdiscordkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruidosujeira%2Fdiscordkit/lists"}