From b2a615828e387662c996c87523bfd08a320af75c Mon Sep 17 00:00:00 2001 From: penguinboi Date: Thu, 30 Jul 2026 23:55:05 -0400 Subject: [PATCH 1/2] add mkdocs and migrate wiki --- .github/workflows/docs.yml | 38 +++++++ .gitignore | 1 + docs/docs/examples/custom-generator.md | 76 +++++++++++++ docs/docs/examples/hot-reload.md | 33 ++++++ docs/docs/examples/index.md | 17 +++ docs/docs/examples/scheduler.md | 46 ++++++++ docs/docs/examples/task-bot.md | 116 +++++++++++++++++++ docs/docs/guides/configuration.md | 90 +++++++++++++++ docs/docs/guides/creating-a-bot.md | 118 ++++++++++++++++++++ docs/docs/guides/database.md | 67 +++++++++++ docs/docs/guides/extensions.md | 123 ++++++++++++++++++++ docs/docs/guides/generators.md | 90 +++++++++++++++ docs/docs/guides/index.md | 79 +++++++++++++ docs/docs/guides/installation.md | 59 ++++++++++ docs/docs/guides/models.md | 148 +++++++++++++++++++++++++ docs/docs/index.md | 146 ++++++++++++++++++++++++ docs/docs/reference/application.md | 12 ++ docs/docs/reference/bot.md | 16 +++ docs/docs/reference/cli.md | 63 +++++++++++ docs/docs/reference/config.md | 17 +++ docs/docs/reference/database.md | 5 + docs/docs/reference/exceptions.md | 5 + docs/docs/reference/generator.md | 22 ++++ docs/docs/reference/importer.md | 5 + docs/docs/reference/model.md | 29 +++++ docs/docs/reference/watcher.md | 7 ++ docs/mkdocs.yml | 110 ++++++++++++++++++ grace/config.py | 4 +- grace/watcher.py | 4 +- pyproject.toml | 5 + 30 files changed, 1547 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/docs/examples/custom-generator.md create mode 100644 docs/docs/examples/hot-reload.md create mode 100644 docs/docs/examples/index.md create mode 100644 docs/docs/examples/scheduler.md create mode 100644 docs/docs/examples/task-bot.md create mode 100644 docs/docs/guides/configuration.md create mode 100644 docs/docs/guides/creating-a-bot.md create mode 100644 docs/docs/guides/database.md create mode 100644 docs/docs/guides/extensions.md create mode 100644 docs/docs/guides/generators.md create mode 100644 docs/docs/guides/index.md create mode 100644 docs/docs/guides/installation.md create mode 100644 docs/docs/guides/models.md create mode 100644 docs/docs/index.md create mode 100644 docs/docs/reference/application.md create mode 100644 docs/docs/reference/bot.md create mode 100644 docs/docs/reference/cli.md create mode 100644 docs/docs/reference/config.md create mode 100644 docs/docs/reference/database.md create mode 100644 docs/docs/reference/exceptions.md create mode 100644 docs/docs/reference/generator.md create mode 100644 docs/docs/reference/importer.md create mode 100644 docs/docs/reference/model.md create mode 100644 docs/docs/reference/watcher.md create mode 100644 docs/mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c91d8ec --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,38 @@ +name: Deploy Docs + +on: + release: + types: [ published ] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install .[doc] + + - name: Build docs + run: cd docs && mkdocs build + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 661e0e0..689250b 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,7 @@ venv.bak/ # mkdocs documentation /site +docs/site/ # mypy .mypy_cache/ diff --git a/docs/docs/examples/custom-generator.md b/docs/docs/examples/custom-generator.md new file mode 100644 index 0000000..02631ee --- /dev/null +++ b/docs/docs/examples/custom-generator.md @@ -0,0 +1,76 @@ +# Custom Generator + +`Generator.templates_path` resolves to `grace/generators/templates/` inside the installed `grace` package, so adding a new `grace generate ` command means contributing it to the framework itself — a new module under `grace/generators/` plus a template under `grace/generators/templates/`. This example walks through adding a `service` generator that scaffolds a plain helper class, following the same shape as the built-in [`CogGenerator`](../reference/generator.md). + +## 1. Add a Template + +`grace/generators/templates/service/{{ service_module_name }}.py`: + +```python +class {{ service_name }}: + def __init__(self): + pass +``` + +## 2. Write the Generator + +`grace/generators/service_generator.py`: + +```python +from logging import info +from re import match + +from click.core import Argument +from jinja2_strcase.jinja2_strcase import to_snake + +from grace.generator import Generator + + +class ServiceGenerator(Generator): + NAME: str = "service" + OPTIONS: dict = { + "params": [ + Argument(["name"], type=str), + ], + } + + def generate(self, name: str): + info(f"Creating service '{name}'") + + self.generate_file( + self.NAME, + variables={ + "service_name": name, + "service_module_name": to_snake(name), + }, + output_dir="bot/helpers", + ) + + def validate(self, name: str, **_kwargs) -> bool: + """A valid service name must be PascalCase.""" + return bool(match(r"^[A-Z][a-zA-Z0-9]*$", name)) + + +def generator() -> Generator: + return ServiceGenerator() +``` + +Because it lives directly under `grace/generators/`, `register_generators` picks it up automatically — no manual registration step. + +## 3. Use It + +```bash +grace generate service Notifier +``` + +Generates `bot/helpers/notifier.py`: + +```python +class Notifier: + def __init__(self): + pass +``` + +## Validating Arguments + +`validate()` runs before `generate()` and can reject bad input before any files are written — return `False` (or raise [`ValidationError`](../reference/exceptions.md)) and `_generate` raises `ValidationError` on your behalf. See [`tests/test_generator.py`](https://github.com/Code-Society-Lab/grace-framework/blob/main/tests/test_generator.py) in the repository for a minimal `Generator` subclass used in Grace's own test suite. diff --git a/docs/docs/examples/hot-reload.md b/docs/docs/examples/hot-reload.md new file mode 100644 index 0000000..9aa5280 --- /dev/null +++ b/docs/docs/examples/hot-reload.md @@ -0,0 +1,33 @@ +# Hot Reload + +During development, restarting your bot after every code change is slow. The `--watch` flag on `grace run` keeps the process alive and reloads your extensions in place whenever a file under `./bot` changes. + +```bash +grace run --watch +``` + +## How It Works + +`--watch` sets `app.watch = True`. On `setup_hook`, `Bot` starts a [`Watcher`](../reference/watcher.md) that observes the `./bot` directory recursively using [watchdog](https://python-watchdog.readthedocs.io/). When a `.py` file is created, modified, or deleted, the corresponding module is reloaded, and `Bot.on_reload` unloads then reloads every extension so the new code takes effect. + +```python +# bot/extensions/task_cog.py — edit and save while the bot is running + +@hybrid_command(name="ping") +async def ping(self, ctx): + await ctx.send("pong") # this shows up next command sync, no restart needed +``` + +## What Gets Reloaded + +The watcher reloads extension modules — everything discovered via `app.extension_modules` (i.e. `bot/extensions/*.py` files exposing `setup`). Changes to models, config files, or code outside `bot/` still require a restart. + +## Disabling It + +`--watch` is off by default (`grace run` alone does not enable it) — it's meant for local development, not production: + +```bash +grace run --no-watch # equivalent to plain `grace run` +``` + +See [Creating a Bot](../guides/creating-a-bot.md#run-your-bot) for the full `grace run` walkthrough. diff --git a/docs/docs/examples/index.md b/docs/docs/examples/index.md new file mode 100644 index 0000000..26bdb3f --- /dev/null +++ b/docs/docs/examples/index.md @@ -0,0 +1,17 @@ +# Examples + +Full walkthroughs demonstrating common Grace Framework patterns. + +| Example | What it shows | +|---------|--------------| +| [Task Bot](task-bot.md) | The complete bot from the [Guides](../guides/index.md) — a model, migrations, and a cog with commands | +| [Custom Generator](custom-generator.md) | Writing your own `grace generate` command | +| [Scheduled Tasks](scheduler.md) | Running recurring jobs with the bot's built-in scheduler | +| [Hot Reload](hot-reload.md) | Using `grace run --watch` during development | + +Each example builds on a project scaffolded with: + +```bash +grace new task-bot +cd task-bot +``` diff --git a/docs/docs/examples/scheduler.md b/docs/docs/examples/scheduler.md new file mode 100644 index 0000000..1393a74 --- /dev/null +++ b/docs/docs/examples/scheduler.md @@ -0,0 +1,46 @@ +# Scheduled Tasks + +Every `Bot` instance carries an [APScheduler](https://apscheduler.readthedocs.io/) `AsyncIOScheduler` as `bot.scheduler`, started automatically in `setup_hook` when the bot connects. Any cog can schedule jobs against it. + +This example extends the [task bot](task-bot.md) with a daily reminder of open tasks. + +`bot/extensions/task_cog.py`: + +```python +from discord.ext.commands import Cog + +from bot.models.task import Task +from grace.bot import Bot + + +class TaskCog(Cog, name="Task"): + def __init__(self, bot: Bot): + self.bot: Bot = bot + + self.bot.scheduler.add_job( + self.remind_open_tasks, + "cron", + hour=9, + id="task_reminder", + ) + + async def remind_open_tasks(self): + open_tasks = Task.where(done=False).all() + + if not open_tasks: + return + + channel = self.bot.get_channel(REMINDER_CHANNEL_ID) + lines = [f"- [{t.id}] **{t.name}**" for t in open_tasks] + + await channel.send("Open tasks:\n" + "\n".join(lines)) + + +async def setup(bot: Bot): + await bot.add_cog(TaskCog(bot)) +``` + +`add_job` accepts any [APScheduler trigger](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html) — `"cron"`, `"interval"`, or `"date"` — with the same keyword arguments you'd use with APScheduler directly, since `bot.scheduler` is an unmodified `AsyncIOScheduler`. + +!!! tip + Give jobs an explicit `id` so re-adding them (e.g. after a `--watch` reload) replaces rather than duplicates the job — see [Hot Reload](hot-reload.md). diff --git a/docs/docs/examples/task-bot.md b/docs/docs/examples/task-bot.md new file mode 100644 index 0000000..0e0c479 --- /dev/null +++ b/docs/docs/examples/task-bot.md @@ -0,0 +1,116 @@ +# Task Bot + +A complete, working task manager bot — the same one built step by step across the [Guides](../guides/index.md). This page shows the finished result end to end. + +## 1. Scaffold the Project + +```bash +grace new task-bot +cd task-bot +``` + +Set your token in `.env`: + +``` +DISCORD_TOKEN=your token here +``` + +## 2. Generate the Model + +```bash +grace generate model Task name:str description:str done:bool +grace db up +``` + +`bot/models/task.py`: + +```python +from grace.model import Field, Model + + +class Task(Model): + id: int | None = Field(default=None, primary_key=True) + name: str + description: str + done: bool +``` + +## 3. Generate the Cog + +```bash +grace generate cog Task "Manage your tasks" +``` + +`bot/extensions/task_cog.py`, filled in with commands: + +```python +from discord.ext.commands import Cog, Context, hybrid_command + +from bot.models.task import Task +from grace.bot import Bot + + +class TaskCog(Cog, name="Task", description="Manage your tasks"): + def __init__(self, bot: Bot): + self.bot: Bot = bot + + @hybrid_command(name="list") + async def list_tasks(self, ctx: Context): + tasks = Task.all() + + if not tasks: + await ctx.send("No tasks found.") + return + + lines = [ + f"- {'✅' if t.done else '❌'} [{t.id}] **{t.name}**: {t.description or ''}" + for t in tasks + ] + await ctx.send("\n".join(lines)) + + @hybrid_command(name="add") + async def add_task(self, ctx: Context, name: str, *, description: str = ""): + task = Task.create(name=name, description=description, done=False) + await ctx.send(f"Added task: **{task.name}**") + + @hybrid_command(name="delete") + async def delete_task(self, ctx: Context, *, task_id: int): + task = Task.find(task_id) + + if not task: + await ctx.send(f"No task found with id '{task_id}'.") + return + + task.delete() + await ctx.send(f"Deleted task: **{task.name}** 🗑️") + + @hybrid_command(name="done") + async def done_task(self, ctx: Context, *, task_id: int): + task = Task.find(task_id) + + if not task: + await ctx.send(f"No task found with id '{task_id}'.") + return + + task.update(done=True) + await ctx.send(f"Marked **{task.name}** as done ✅") + + +async def setup(bot: Bot): + await bot.add_cog(TaskCog(bot)) +``` + +## 4. Run It + +```bash +grace run --watch +``` + +``` +/add "Write docs" Finish the getting started guide +/list +/done 1 +/delete 1 +``` + +See [Models & Migrations](../guides/models.md) and [Extensions](../guides/extensions.md) for the full explanation of each piece. diff --git a/docs/docs/guides/configuration.md b/docs/docs/guides/configuration.md new file mode 100644 index 0000000..39a7d91 --- /dev/null +++ b/docs/docs/guides/configuration.md @@ -0,0 +1,90 @@ +# Configuration + +Every generated bot ships with three configuration files under `config/`, all read by [`Config`](../reference/config.md) and combined by [`Application`](../reference/application.md): + +| File | Purpose | +|---|---| +| `config/settings.cfg` | Client identity (name, prefix, description, guild) and the Discord token | +| `config/database.cfg` | Per-environment database connection settings | +| `config/environment.cfg` | Per-environment logging and SQLAlchemy echo settings | + +Grace uses three environments — `production`, `development`, and `test` — selected via the `GRACE_ENV` environment variable. If unset, `development` is used by default (see `Application.load`). + +## Environment Variable Interpolation + +Config values can reference environment variables (including ones loaded from a `.env` file) using `${NAME}` syntax. This is handled by [`EnvironmentInterpolation`](../reference/config.md): + +```ini +token = ${DISCORD_TOKEN} +``` + +`DISCORD_TOKEN` is read from `.env` or the shell environment. If the variable isn't set, the value is left empty rather than raising an error. + +## `settings.cfg` + +```ini +[client] +name = task-bot +prefix = :: +description = task-bot +guild_id = ${GUILD_ID} + +[discord] +; Although it is possible to set directly your discord token here, we recommend, for security reasons, that you set +; your discord token as an environment variable called 'DISCORD_TOKEN'. +token = ${DISCORD_TOKEN} +``` + +- `[client]` is exposed as `Bot.config` inside your bot (an alias for `app.client`) and is used to build the command prefix, description, and optional guild-restricted command sync. +- `[discord]` `token` is what `Application` reads on startup and exposes as `app.token`. + +## `database.cfg` + +```ini +[database.production] +url = ${DATABASE_URL} + +[database.development] +adapter = sqlite +database = task_bot_development.db + +[database.test] +adapter = sqlite +database = task_bot_test.db +``` + +Each section is named `database.`. You need at minimum an `adapter` (the SQL dialect, optionally `dialect+driver`, e.g. `postgresql+psycopg2`) and a `database` name — or a full `url` (as used for `production` above, letting you supply a complete SQLAlchemy connection string via an environment variable). Optional keys: `user`, `password`, `host`, `port`. + +See the [SQLAlchemy dialects reference](https://docs.sqlalchemy.org/en/14/dialects/index.html) for supported adapters, and the [engine configuration guide](https://docs.sqlalchemy.org/en/14/core/engines.html) for further details. + +## `environment.cfg` + +```ini +[production] +log_level = INFO +sqlalchemy_echo = False + +[development] +log_level = DEBUG +sqlalchemy_echo = True + +[test] +log_level = ERROR +sqlalchemy_echo = True +``` + +`log_level` controls both the file logger (`logs/.log`) and the console logger. `sqlalchemy_echo` toggles SQL statement logging, which is useful while developing models and queries. + +## Reading Config Values in Your Code + +Anywhere you have access to `app.config` (a [`Config`](../reference/config.md) instance), you can read arbitrary values: + +```python +guild_id = app.config.get("client", "guild_id") +``` + +`Config.get` automatically evaluates numeric, boolean, and list-looking values, so `guild_id = 123456789012345678` in your `.cfg` file comes back as an `int`, not a string. + +## Next Steps + +With configuration in place, move on to [Models & Migrations](models.md) to give your bot something to store. diff --git a/docs/docs/guides/creating-a-bot.md b/docs/docs/guides/creating-a-bot.md new file mode 100644 index 0000000..5100cc8 --- /dev/null +++ b/docs/docs/guides/creating-a-bot.md @@ -0,0 +1,118 @@ +# Creating a Bot + +Grace includes a utility script to simplify interaction with your bot. Running `grace --help` displays all available commands. Outside of a project directory, only `grace new` is available — the rest require a generated project to run from (they import `bot.app`/`bot.bot`, see the [CLI reference](../reference/cli.md)). + +## Generate Your Bot + +Throughout this guide, we'll create a bot called `task-bot`: + +```bash +grace new task-bot +``` + +Switch to its directory: + +```bash +cd task-bot +``` + +## Directory Structure + +The generated project includes: + +| File/Folder | Description | +|---|---| +| `alembic.ini` | Alembic configuration file for migrations | +| `bot/` | Contains your bot logic — primary work area | +| `bot/extensions/` | Cogs generated with `grace generate cog` live here | +| `bot/models/` | Models generated with `grace generate model` live here | +| `bot/helpers/` | Shared helper code for your bot | +| `config/` | Configuration files and settings (see [Configuration](configuration.md)) | +| `db/` | Database migrations, seeds, and Alembic environment | +| `lib/` | Shared libraries or utilities | +| `logs/` | Log files generated by the application | +| `README.md` | Project overview and documentation | +| `pyproject.toml` | Project packaging configuration file | +| `.env` | Environment variables file (hidden) | + +The `bot/__init__.py` file wires everything together — it creates the `Application` and `Bot` instances that the `grace` CLI imports whenever it runs a command inside your project: + +```python +from grace.application import Application + + +def _create_bot(app): + from bot.task_bot import TaskBot + return TaskBot(app) + + +app = Application() +bot = _create_bot(app) +``` + +Your bot's class itself lives in `bot/task_bot.py` and subclasses [`grace.bot.Bot`](../reference/bot.md): + +```python +from logging import info + +from grace.bot import Bot + + +class TaskBot(Bot): + async def on_ready(self): + info(f"{self.user.name}:#{self.user.id} is online and ready to use!") +``` + +## Building and Running Your Bot + +### Minimal Configuration + +Your bot requires a Discord token to function. To obtain one: + +1. Create a Discord application at the [Discord Developer Portal](https://discord.com/developers/applications) +2. Copy your bot's token +3. Paste it in `.env` after `DISCORD_TOKEN=` + +``` +DISCORD_TOKEN=Paste your token here +``` + +!!! warning "Security Warning" + Do not share your token or `.env` file. Reset it immediately if it's accidentally revealed. + +Remember to invite your bot into your Discord server for testing. + +### Run Your Bot + +Launch your bot with: + +```bash +grace run --watch +``` + +The `--watch` flag enables hot reload — see [`Watcher`](../reference/watcher.md) for how it works. + +If configured correctly, the output will look like: + +``` +[2025-04-18 01:22:25] development _show_application_info cli INFO +| Discord.py version: 2.5.2 +| PID: 895923 +| Environment: development +| Syncing command: True +| Watcher enabled: True +| Using database: task_bot_development.db with sqlite +[2025-04-18 01:22:25] development __init__ selector_events DEBUG Using selector: EpollSelector +2025-04-18 01:22:25 WARNING discord.ext.commands.bot Privileged message content intent is missing, commands may not work as expected. +[2025-04-18 01:22:25] development _async_setup_hook bot WARNING Privileged message content intent is missing, commands may not work as expected. +2025-04-18 01:22:25 INFO discord.client logging in using static token +[2025-04-18 01:22:25] development login client INFO logging in using static token +[2025-04-18 01:22:26] development setup_hook bot WARNING Syncing application commands. This may take some time. +2025-04-18 01:22:26 INFO discord.gateway Shard ID None has connected to Gateway (Session ID: 79ad3f472e8e83f31ee61a522a38150f). +[2025-04-18 01:22:26] development received_message gateway INFO Shard ID None has connected to Gateway (Session ID: 79ad3f472e8e83f31ee61a522a38150f). +[2025-04-18 01:22:28] development on_ready task_bot INFO TaskBot:#123456789123456789 is online and ready to use! +``` + +## Next Steps + +Now that your bot is running, give it something to do — start with [Models & Migrations](models.md) to define a `Task` model, then [Extensions](extensions.md) to add commands. diff --git a/docs/docs/guides/database.md b/docs/docs/guides/database.md new file mode 100644 index 0000000..6aa0f07 --- /dev/null +++ b/docs/docs/guides/database.md @@ -0,0 +1,67 @@ +# Database Management + +Grace exposes a `grace db` command group for managing your bot's database, plus automatic database creation on startup. + +## Create + +```bash +grace db create +``` + +Creates the database itself (the file for SQLite, the schema for other dialects) if it doesn't already exist. This does **not** create tables — run [migrations](models.md#database-migrations) with `grace db up` for that. If the database already exists, this is a no-op with a warning. + +!!! tip + `grace run` automatically creates the database on startup if it's missing, so you rarely need to run `grace db create` yourself. + +## Migrate + +```bash +grace db up [REVISION] +grace db down [REVISION] +``` + +Applies (`up`) or reverts (`down`) Alembic migrations from `db/alembic/versions/`. `REVISION` defaults to `head`. See [Models & Migrations](models.md#database-migrations) for how migrations are generated. + +## Seed + +```bash +grace db seed +``` + +Imports `db/seed.py` from your project and calls `seed_database()`. This is a stub generated with your project — fill it in with whatever initial data your bot needs: + +```python +# db/seed.py +from bot.models.task import Task + +def seed_database(): + Task.create(name="Example task", description="Created by the seed script", done=False) +``` + +If you have multiple seed sources, consider organizing them under a `db/seeds/` directory and importing them from `seed_database()`. + +## Drop + +```bash +grace db drop +``` + +Drops all tables, then drops the database itself. If the database doesn't exist, this is a no-op with a warning. + +!!! warning + This is destructive and unrecoverable outside of your migrations/backups — use with care, especially against a shared database. + +## Choosing an Environment + +Every `grace` command inside a project accepts `--environment`, which selects the `[database.]` section from `config/database.cfg` (see [Configuration](configuration.md)): + +```bash +grace --environment test db create +grace --environment test db up +``` + +If omitted, the `GRACE_ENV` environment variable is used, falling back to `development`. + +## Next Steps + +To extend `grace generate` with your own scaffolding commands, see [Writing Generators](generators.md). diff --git a/docs/docs/guides/extensions.md b/docs/docs/guides/extensions.md new file mode 100644 index 0000000..0d97915 --- /dev/null +++ b/docs/docs/guides/extensions.md @@ -0,0 +1,123 @@ +# Extensions + +Now that we have a model and table for storing tasks, let's give the bot a way to interact with them using **extensions** — modules that group related commands together, discovered and loaded automatically at startup. + +An extension is simply a `bot/extensions/*.py` module that defines an async `setup(bot)` function and, conventionally, a [`discord.ext.commands.Cog`](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html) subclass. Grace has no custom base class for cogs — it re-exports `discord.ext.commands` directly (`from grace import Cog, hybrid_command, ...`), and discovers any module under `bot/extensions/` that exposes `setup` (see `Application.extension_modules`). + +## Generating a Cog + +Generate your first cog: + +```bash +grace generate cog Task +``` + +This creates a new module at `bot/extensions/task_cog.py`: + +```python +from discord.ext.commands import Cog +from grace.bot import Bot + +class TaskCog(Cog, name="Task"): + def __init__(self, bot: Bot): + self.bot: Bot = bot + +async def setup(bot: Bot): + await bot.add_cog(TaskCog(bot)) +``` + +The cog name must be PascalCase (e.g. `Task`, `TaskManager`). You can optionally pass a description: + +```bash +grace generate cog Task "Manage your tasks" +``` + +This is the starting point for your command logic. `TaskCog` is registered automatically when your bot starts (`Bot.load_extensions`), and you define commands inside it to interact with your models. + +## Adding Commands + +Start by importing what you need: + +```python +from discord.ext.commands import Cog, Context, hybrid_command +from bot.models.task import Task +``` + +### Listing Tasks + +```python +@hybrid_command(name="list") +async def list_tasks(self, ctx: Context): + tasks = Task.all() + + if not tasks: + await ctx.send("No tasks found.") + return + + lines = [f"- {'✅' if t.done else '❌'} [{t.id}] **{t.name}**: {t.description or ''}" for t in tasks] + await ctx.send("\n".join(lines)) +``` + +This fetches all `Task` records and sends them as a formatted message. + +### Adding a Task + +```python +@hybrid_command(name="add") +async def add_task(self, ctx: Context, name: str, *, description: str = ""): + task = Task.create(name=name, description=description, done=False) + await ctx.send(f"Added task: **{task.name}**") +``` + +Users can type: +``` +/add "Write docs" Finish the getting started guide +``` + +### Deleting a Task + +```python +@hybrid_command(name="delete") +async def delete_task(self, ctx: Context, *, task_id: int): + task = Task.find(task_id) + + if not task: + await ctx.send(f"No task found with id '{task_id}'.") + return + + task.delete() + await ctx.send(f"Deleted task: **{task.name}** 🗑️") +``` + +### Marking a Task as Done + +```python +@hybrid_command(name="done") +async def done_task(self, ctx: Context, *, task_id: int): + task = Task.find(task_id) + + if not task: + await ctx.send(f"No task found with id '{task_id}'.") + return + + task.update(done=True) + await ctx.send(f"Marked **{task.name}** as done ✅") +``` + +### Trying It Out + +Since `hybrid_command` registers both a slash command and a text command, you can test with either: + +``` +/add "Write docs" Finish the migration section +/list +/done 1 +::list +::delete 1 +``` + +(`::` is the default prefix set in `config/settings.cfg` — see [Configuration](configuration.md).) + +## Next Steps + +Once your commands are working, learn how to manage the underlying database from the CLI in [Database Management](database.md), or extend the generator system itself in [Writing Generators](generators.md). diff --git a/docs/docs/guides/generators.md b/docs/docs/guides/generators.md new file mode 100644 index 0000000..ea7b148 --- /dev/null +++ b/docs/docs/guides/generators.md @@ -0,0 +1,90 @@ +# Writing Generators + +`grace generate cog`, `grace generate model`, `grace generate migration`, and `grace generate project` (used internally by `grace new`) are all built on the same extension point: [`grace.generator.Generator`](../reference/generator.md). Any module you drop into `grace/generators/`, or into your own package registered the same way, that exposes a module-level `generator()` function is automatically discovered and added under `grace generate `. + +## Anatomy of a Generator + +```python +from grace.generator import Generator + + +class MyGenerator(Generator): + NAME = "my_generator" + + def generate(self, *args, **kwargs): + # Implement the generate method here + ... + + +def generator() -> Generator: + return MyGenerator() +``` + +- `NAME` becomes the subcommand name: `grace generate my_generator`. +- `generate()` does the actual work and is called after `validate()` passes. +- `validate()` (optional) returns `False` to reject the arguments before `generate()` runs; a failed validation raises `ValidationError`. +- `OPTIONS` lets you customize the underlying [`click.Command`](https://click.palletsprojects.com/) — most commonly to declare positional arguments. +- Inside `generate()`/`validate()`, `self.app` gives you the current [`Application`](../reference/application.md) instance. + +## Example: A Real Generator + +Here's the actual `CogGenerator` (`grace/generators/cog_generator.py`), which backs `grace generate cog`: + +```python +from logging import info +from re import match + +from click.core import Argument +from jinja2_strcase.jinja2_strcase import to_snake + +from grace.generator import Generator + + +class CogGenerator(Generator): + NAME: str = "cog" + OPTIONS: dict = { + "params": [ + Argument(["name"], type=str), + Argument(["description"], type=str, required=False, default=""), + ], + } + + def generate(self, name: str, description: str = ""): + info(f"Creating cog '{name}'") + + self.generate_file( + self.NAME, + variables={ + "cog_name": name, + "cog_module_name": to_snake(name), + "cog_description": description, + }, + output_dir="bot/extensions", + ) + + def validate(self, name: str, **_kwargs) -> bool: + """A valid cog name must be PascalCase (letters and numbers only).""" + return bool(match(r"^[A-Z][a-zA-Z0-9]*$", name)) + + +def generator() -> Generator: + return CogGenerator() +``` + +## Rendering Output + +`Generator` gives you two ways to produce files, both rooted at `grace/generators/templates/`: + +- **`generate_file(template_dir, variables, output_dir)`** — renders a single [Jinja2](https://jinja.palletsprojects.com/) template file (used by `cog` and `model`). The template directory's file is expected to be named with Jinja2 syntax too (e.g. `{{ cog_module_name }}_cog.py`), so both the filename and the contents get rendered from `variables`. Two extra filters are available: `camel_case_to_space` and `pluralize` (via [`jinja2-strcase`](https://pypi.org/project/jinja2-strcase/) and [`inflect`](https://pypi.org/project/inflect/)). +- **`generate_template(template_dir, variables)`** — runs an entire directory through [Cookiecutter](https://cookiecutter.readthedocs.io/) (used by `project`), for scaffolding multi-file/multi-directory output. + +## Registration + +Generators are discovered by [`register_generators`](../reference/generator.md), which is called automatically whenever `grace` starts inside a project (or before `grace new`, for the bare CLI). It imports every module under `grace.generators` and registers whatever `generator()` returns onto the `generate` command group. + +!!! note + `Generator.templates_path` always resolves to `grace/generators/templates/` inside the **installed** `grace` package — both `generate_file` and `generate_template` render from there. This means a new generator, and its templates, currently need to live inside the `grace` package itself (i.e. contributed to the framework), rather than dropped into an individual bot project. See the [Custom Generator example](../examples/custom-generator.md) for what that looks like in practice. + +## Next Steps + +Browse the [Reference](../reference/generator.md) section for the full `Generator` API, or look at the [Custom Generator example](../examples/custom-generator.md) for a complete, runnable generator. diff --git a/docs/docs/guides/index.md b/docs/docs/guides/index.md new file mode 100644 index 0000000..cdb4b8c --- /dev/null +++ b/docs/docs/guides/index.md @@ -0,0 +1,79 @@ +# Welcome to **Grace Framework** + +Grace Framework is an opinionated, extensible Discord bot framework built on top of [discord.py](https://github.com/Rapptz/discord.py). It is designed to help developers rapidly build scalable, feature-rich Discord bots with minimal boilerplate. + +#### Key Features +- Quick to start: generate a full-featured bot in seconds +- Modular architecture: clean separation of features via extensions (cogs) +- Database integration: connect your bot to a persistent backend with a single config +- Built-in generators: create extensions, models, and migrations with a single command + +#### Inspiration + +Grace Framework was inspired by the community Discord bot, [Grace](https://github.com/Code-Society-Lab/grace), that evolved into a modular and powerful bot — it began to resemble a standalone framework. Recognizing its potential, the developers extracted its architecture into Grace Framework, making its ease-of-use and flexibility available to other developers. + +## What These Guides Cover + +Throughout these guides, we build a bot called `task-bot` — a simple task manager. It will be able to: + +- List all current tasks — `/list` +- Add new tasks — `/add ` +- Remove existing tasks — `/delete ` +- Mark tasks as complete — `/done ` + +Along the way you'll learn: + +- [Installation](installation.md) — installing Grace Framework and its requirements +- [Creating a Bot](creating-a-bot.md) — scaffolding a project, the generated layout, and running your bot +- [Configuration](configuration.md) — how `config/*.cfg` files and environments work +- [Models & Migrations](models.md) — defining database models and evolving your schema with Alembic +- [Extensions](extensions.md) — organizing commands into cogs +- [Database Management](database.md) — creating, dropping, and seeding your database from the CLI +- [Writing Generators](generators.md) — extending `grace generate` with your own generators + +## Quick Start + +Install Grace Framework: +```bash +pip install grace-framework +``` + +Generate a new bot: +```bash +grace new task-bot +cd task-bot +``` + +Set your bot token in `.env`: +``` +DISCORD_TOKEN=your token here +``` + +Run it: +```bash +grace run +``` + +For the full walkthrough — including models, migrations, and cogs — start with [Installation](installation.md). + +## Resources + +Here's a list of resources that might be useful when working with **Grace Framework**: + +- [Python's Docs](https://docs.python.org/3/) +- [discord.py](https://discordpy.readthedocs.io/) +- [Discord Developer Portal](https://discord.com/developers/docs/intro) +- [SQLAlchemy](https://docs.sqlalchemy.org/) +- [SQLModel](https://sqlmodel.tiangolo.com/) +- [Alembic](https://alembic.sqlalchemy.org/) +- [Pytest](https://docs.pytest.org/) + +## Contributing + +We welcome any contributions, whether it's fixing bugs, suggesting features, or improving the docs — every bit helps: + +- [Submit an issue](https://github.com/Code-Society-Lab/grace-framework/issues) +- [Open a pull request](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) +- Or hop into our [Discord community](https://discord.gg/code-society-823178343943897088) and say hi! + +If you intend to contribute, please read the [CONTRIBUTING.md](https://github.com/Code-Society-Lab/grace-framework/blob/main/CONTRIBUTING.md) first. Additionally, **every contributor** is expected to follow the [code of conduct](https://github.com/Code-Society-Lab/grace-framework/blob/main/CODE_OF_CONDUCT.md). diff --git a/docs/docs/guides/installation.md b/docs/docs/guides/installation.md new file mode 100644 index 0000000..b74a1e0 --- /dev/null +++ b/docs/docs/guides/installation.md @@ -0,0 +1,59 @@ +# Installation + +## Prerequisites + +Before beginning, ensure you have: + +- Python 3.12+ +- pip +- SQLite (default database, no setup needed) + - PostgreSQL, MySQL, MariaDB, Oracle, and MS-SQL are also supported with configuration + +## Virtual Environment + +It is recommended to use a virtual environment to manage project dependencies. + +### What is a Virtual Environment? + +A virtual environment is an isolated Python environment that allows you to isolate the dependencies required by different projects from each other. This prevents version conflicts between projects and system tools. + +### Create and Activate Your Virtual Environment + +Using `venv`: + +```bash +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +``` + +## Install Grace Framework + +To install Grace Framework: + +```bash +pip install grace-framework +``` + +### Installing from Source (For Development) + +To install the development version: + +=== "Linux/Windows" + + ```bash + git clone https://github.com/Code-Society-Lab/grace-framework.git + cd grace-framework + pip install -e .[dev] + ``` + +=== "macOS" + + ```bash + git clone https://github.com/Code-Society-Lab/grace-framework.git + cd grace-framework + pip install -e ".[dev]" + ``` + +## Next Steps + +With Grace Framework installed, you're ready to [generate your first bot](creating-a-bot.md). diff --git a/docs/docs/guides/models.md b/docs/docs/guides/models.md new file mode 100644 index 0000000..bea2b15 --- /dev/null +++ b/docs/docs/guides/models.md @@ -0,0 +1,148 @@ +# Models & Migrations + +Models are how your bot talks to its database. Grace models are [`SQLModel`](https://sqlmodel.tiangolo.com/) classes with an ActiveRecord-style API layered on top by [`grace.model.Model`](../reference/model.md). + +## Generating a Model + +Generate your first model: + +```bash +grace generate model Task name:str description:str done:bool +``` + +This command automatically: + +- Creates a new database migration in `db/alembic/versions/` +- Generates a new model class in `bot/models/task.py` +- Registers the model for use with the built-in ORM utilities + +Example output: + +``` +[2025-10-18 21:02:21] development generate model_generator INFO Generating model 'Task' +[2025-10-18 21:02:21] development __init__ migration INFO Context impl SQLiteImpl. +[2025-10-18 21:02:21] development __init__ migration INFO Will assume non-transactional DDL. +[2025-10-18 21:02:21] development _compare_tables compare INFO Detected added table 'task' +Generating /db/alembic/versions/99e6d0cf0aec_create_task.py ... done +``` + +!!! note + Column definitions are currently limited to basic Python/SQLAlchemy types (`str`, `int`, `float`, `bool`, `String`, `Integer`, `Boolean`, etc.). + +The generated `bot/models/task.py`: + +```python +from grace.model import Field, Model + + +class Task(Model): + id: int | None = Field(default=None, primary_key=True) + name: str + description: str + done: bool +``` + +You don't need to pass `table=True` yourself — Grace's model metaclass sets it automatically for every `Model` subclass. + +## Database Migrations + +Grace uses [Alembic](https://alembic.sqlalchemy.org/) to manage schema migrations. When you generate a model (or run `grace generate migration`), Grace autogenerates a migration by diffing your models against the current database schema, writing the result to `db/alembic/versions/`. + +Example migration file: + +```python +"""Create Task +Revision ID: 99e6d0cf0aec +Revises: +Create Date: 2025-10-18 21:02:21.978903 +""" +import sqlmodel +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '99e6d0cf0aec' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade() -> None: + op.create_table('task', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('done', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + +def downgrade() -> None: + op.drop_table('task') +``` + +Directory layout: + +``` +db/ +├── alembic/ +│ ├── versions/ +│ │ └── 99e6d0cf0aec_create_task.py +│ ├── env.py +│ └── script.py.mako +``` + +### Writing a Migration Manually + +If you change a model by hand (add a column, rename a field, etc.) without regenerating it, create a migration for just that change: + +```bash +grace generate migration "Add priority to Task" +``` + +### Running Migrations + +Apply pending migrations to your database: + +```bash +grace db up +``` + +``` +[2025-10-18 21:05:42] development db_up INFO Applying migration 99e6d0cf0aec_create_task.py +[2025-10-18 21:05:42] development db_up INFO Upgrade successful. +``` + +Roll back with `grace db down`. Both commands accept an optional revision (default `head`) — see [Database Management](database.md). + +## Querying Models + +Every `Model` subclass gets a fluent, chainable [`Query`](../reference/model.md) API directly on the class: + +```python +from bot.models.task import Task + +# Create +task = Task.create(name="Write docs", description="Finish the guide", done=False) + +# Find +Task.find(1) # by primary key +Task.find_by(name="Write docs") # first match by equality + +# Query, filter, and order +Task.where(Task.done == False).order_by(Task.id.desc()).limit(10).all() +Task.not_(done=True).all() +Task.with_("comments").where(Task.done == True).all() + +# Update / delete / reload +task.update(done=True) +task.reload() +task.delete() + +# Aggregate +Task.count() +``` + +See the [`Model` reference](../reference/model.md) for the full list of query and persistence methods. + +## Next Steps + +Now let's expose these tasks to Discord users with [Extensions](extensions.md). diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 0000000..939024f --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,146 @@ +
+ Build powerful Discord bots, without the boilerplate. +
+ +
+ +[Get Started](guides/installation.md){ .md-button .md-button--primary } +[Reference](reference/bot.md){ .md-button } + +
+ +
+ +

+ + Join Discord + + + + Tests + + + + PyPI + +

+ +
+--- + +Grace Framework is an opinionated, extensible Discord bot framework built on top of [discord.py](https://github.com/Rapptz/discord.py). It comes with the tools you need to rapidly build scalable, feature-rich Discord bots with minimal boilerplate. + +- **Quick to start** — generate a full-featured bot in seconds +- **Modular architecture** — clean separation of features via extensions (cogs) +- **Database integration** — connect your bot to a persistent backend with SQLModel and Alembic migrations +- **Built-in generators** — scaffold extensions, models, and migrations with a single command + +## Quickstart + +=== "Install" + + **Requirements:** Python 3.12+ + + ```bash + pip install grace-framework + ``` + + Using a virtual environment is strongly recommended: + + ```bash + python -m venv venv + source venv/bin/activate # Windows: venv\Scripts\activate + pip install grace-framework + ``` + +=== "Generate" + + Scaffold a new bot project: + + ```bash + grace new my-awesome-bot + cd my-awesome-bot + ``` + + Set your bot token in `.env`: + + ``` + DISCORD_TOKEN=your token here + ``` + +=== "Run" + + ```bash + grace run + ``` + + Add `--watch` during development to enable hot reload: + + ```bash + grace run --watch + ``` + +## Where to go next + +
+ +- :fontawesome-solid-book-open: **Guides** + + --- + + Step-by-step tutorials covering installation, models, extensions, configuration, and more. + + [:octicons-arrow-right-24: Start with Installation](guides/installation.md) + +- :fontawesome-solid-code: **Reference** + + --- + + Complete API documentation for every class and function in the framework. + + [:octicons-arrow-right-24: Browse the Reference](reference/bot.md) + +- :fontawesome-solid-terminal: **Examples** + + --- + + Full walkthroughs demonstrating common patterns and use cases. + + [:octicons-arrow-right-24: View Examples](examples/index.md) + +- :fontawesome-brands-github: **Source Code** + + --- + + Browse the source, open issues, and contribute on GitHub. + + [:octicons-arrow-right-24: View on GitHub](https://github.com/Code-Society-Lab/grace-framework) + +
+ +## Inspiration + +Grace Framework was inspired by the evolution of our community Discord bot, [Grace](https://github.com/Code-Society-Lab/grace), which grew to support modular extensions, database integrations, and rapid feature development — it began to resemble a standalone framework. Recognizing its potential, we extracted its architecture into Grace Framework, making its ease-of-use and flexibility available to developers everywhere. + +## Contributing + +We welcome everyone to contribute! Whether it's fixing bugs, suggesting features, or improving the docs — every bit helps. + +- [Submit an issue](https://github.com/Code-Society-Lab/grace-framework/issues) +- [Open a pull request](https://github.com/Code-Society-Lab/grace-framework/blob/main/CONTRIBUTING.md) +- Hop into our [Discord community](https://discord.gg/code-society-823178343943897088) and say hi! + +Please read the [CONTRIBUTING.md](https://github.com/Code-Society-Lab/grace-framework/blob/main/CONTRIBUTING.md) and follow the [code of conduct](https://github.com/Code-Society-Lab/grace-framework/blob/main/CODE_OF_CONDUCT.md). + +## License + +Released under the [MIT License](https://github.com/Code-Society-Lab/grace-framework/blob/main/LICENSE). diff --git a/docs/docs/reference/application.md b/docs/docs/reference/application.md new file mode 100644 index 0000000..40f0ffe --- /dev/null +++ b/docs/docs/reference/application.md @@ -0,0 +1,12 @@ +# Application + +The `Application` class is the core of every Grace bot. It manages configuration, the database engine and session, model discovery, extension discovery, and logging. Every generated project instantiates exactly one `Application` in `bot/__init__.py`, alongside the `Bot` that wraps it. + +```python +from grace.application import Application + +app = Application() +app.load() # sets the environment and wires up logging, models, and the database +``` + +::: grace.application.Application diff --git a/docs/docs/reference/bot.md b/docs/docs/reference/bot.md new file mode 100644 index 0000000..94e9d7e --- /dev/null +++ b/docs/docs/reference/bot.md @@ -0,0 +1,16 @@ +# Bot + +The `Bot` class is a subclass of [`discord.ext.commands.Bot`](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#bot) and drives your bot's lifecycle: loading extensions, syncing application commands, running the scheduler, and — when enabled — hot-reloading on file changes. + +```python +from logging import info + +from grace.bot import Bot + + +class TaskBot(Bot): + async def on_ready(self): + info(f"{self.user.name}:#{self.user.id} is online and ready to use!") +``` + +::: grace.bot.Bot diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md new file mode 100644 index 0000000..faaa0ac --- /dev/null +++ b/docs/docs/reference/cli.md @@ -0,0 +1,63 @@ +# CLI + +The `grace` command (entry point `grace.cli:main`) is your interface to Grace Framework. Outside of a generated project, only `grace new` is available. Once inside a project (i.e. a directory containing an importable `bot` package), the full command set is available. + +```bash +grace --help +``` + +## Global Options + +Available on every in-project command (must come before the subcommand): + +| Option | Description | +|---|---| +| `--environment TEXT` | The environment to load (`production`, `development`, `test`). Falls back to the `GRACE_ENV` environment variable, then `development`. | + +```bash +grace --environment test db up +``` + +## Commands + +### `grace new NAME` + +Scaffolds a new bot project named `NAME` in a directory of the same name, via [`grace generate project`](../guides/generators.md). Available outside of a project. + +### `grace run` + +Runs the bot. Automatically creates the database if it doesn't exist. + +| Option | Default | Description | +|---|---|---| +| `--sync / --no-sync` | `--sync` | Sync application (slash) commands on startup. | +| `--watch / --no-watch` | `--no-watch` | Enable hot reload — see [`Watcher`](watcher.md). | + +```bash +grace run --watch +``` + +### `grace db` + +Database management commands — see [Database Management](../guides/database.md) for details on each. + +| Command | Arguments | Description | +|---|---|---| +| `grace db create` | — | Creates the database if it doesn't exist. | +| `grace db drop` | — | Drops all tables, then drops the database. | +| `grace db seed` | — | Runs `db/seed.py`'s `seed_database()`. | +| `grace db up [REVISION]` | `REVISION` (default `head`) | Applies migrations. | +| `grace db down [REVISION]` | `REVISION` (default `head`) | Reverts migrations. | + +### `grace generate` + +Scaffolding commands — see [Writing Generators](../guides/generators.md) for how these are registered, and [Models & Migrations](../guides/models.md#generating-a-model) / [Extensions](../guides/extensions.md#generating-a-cog) for usage. + +| Command | Arguments | Description | +|---|---|---| +| `grace generate cog NAME [DESCRIPTION]` | `NAME` (PascalCase), `DESCRIPTION` (optional) | Generates `bot/extensions/_cog.py`. | +| `grace generate model NAME [COLUMN:TYPE ...]` | `NAME` (PascalCase), variadic `COLUMN:TYPE` pairs | Generates `bot/models/.py` and an initial migration. | +| `grace generate migration MESSAGE` | `MESSAGE` | Autogenerates an Alembic revision from the current model state. | +| `grace generate project NAME` | `NAME` (lowercase/hyphens) | Scaffolds a new project directory (used internally by `grace new`). | + +`grace generate` is a dynamic command group — any module under `grace.generators` (or a package you register the same way) exposing a `generator()` function shows up here automatically. diff --git a/docs/docs/reference/config.md b/docs/docs/reference/config.md new file mode 100644 index 0000000..01a23a6 --- /dev/null +++ b/docs/docs/reference/config.md @@ -0,0 +1,17 @@ +# Config + +`Config` loads and exposes your project's `config/*.cfg` files for the currently selected environment. See the [Configuration guide](../guides/configuration.md) for a walkthrough of `settings.cfg`, `database.cfg`, and `environment.cfg`. + +```python +from grace.application import Application + +app = Application() +app.load() + +app.config.get("client", "guild_id") +app.config.database_uri +``` + +::: grace.config.Config + +::: grace.config.EnvironmentInterpolation diff --git a/docs/docs/reference/database.md b/docs/docs/reference/database.md new file mode 100644 index 0000000..65fa41f --- /dev/null +++ b/docs/docs/reference/database.md @@ -0,0 +1,5 @@ +# Database + +Thin wrappers around [Alembic](https://alembic.sqlalchemy.org/)'s `revision`, `upgrade`, and `downgrade` commands, scoped to the current `Application`'s environment and `alembic.ini`. These back the `grace generate migration` and `grace db up`/`grace db down` commands — see [Database Management](../guides/database.md). + +::: grace.database diff --git a/docs/docs/reference/exceptions.md b/docs/docs/reference/exceptions.md new file mode 100644 index 0000000..bbf98aa --- /dev/null +++ b/docs/docs/reference/exceptions.md @@ -0,0 +1,5 @@ +# Exceptions + +All exceptions raised by Grace derive from `GraceError`. + +::: grace.exceptions diff --git a/docs/docs/reference/generator.md b/docs/docs/reference/generator.md new file mode 100644 index 0000000..a6c1eaf --- /dev/null +++ b/docs/docs/reference/generator.md @@ -0,0 +1,22 @@ +# Generator + +The base class for every `grace generate` subcommand, plus the discovery function that registers them. See [Writing Generators](../guides/generators.md) for a full guide. + +```python +from grace.generator import Generator + + +class MyGenerator(Generator): + NAME = "my_generator" + + def generate(self, *args, **kwargs): + ... + + +def generator() -> Generator: + return MyGenerator() +``` + +::: grace.generator.Generator + +::: grace.generator.register_generators diff --git a/docs/docs/reference/importer.md b/docs/docs/reference/importer.md new file mode 100644 index 0000000..4c5a365 --- /dev/null +++ b/docs/docs/reference/importer.md @@ -0,0 +1,5 @@ +# Importer + +Utilities for discovering and importing modules within a package — used internally to find your project's models, extensions, and generators. + +::: grace.importer diff --git a/docs/docs/reference/model.md b/docs/docs/reference/model.md new file mode 100644 index 0000000..fbb80cf --- /dev/null +++ b/docs/docs/reference/model.md @@ -0,0 +1,29 @@ +# Model + +`Model` is the ActiveRecord-style base class every generated model subclasses. It combines [SQLModel](https://sqlmodel.tiangolo.com/) with a fluent [`Query`](#grace.model.Query) builder, accessible both from instances (`task.save()`) and directly on the class (`Task.where(...)`) via the `_ModelMeta` metaclass. + +```python +from typing import Optional +from grace.model import Field, Model + + +class User(Model): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + email: str + age: int + active: bool = True + + +user = User.create(name="Test", email="test@example.com", age=20) +User.find(1) +User.find_by(name="Alice") +User.where(User.age > 25, active=True).order_by(User.age.desc()).limit(10).all() +User.with_("posts", "comments").where(User.active == True).all() +``` + +See [Models & Migrations](../guides/models.md) for the full guide. + +::: grace.model.Model + +::: grace.model.Query diff --git a/docs/docs/reference/watcher.md b/docs/docs/reference/watcher.md new file mode 100644 index 0000000..2ee9600 --- /dev/null +++ b/docs/docs/reference/watcher.md @@ -0,0 +1,7 @@ +# Watcher + +Powers `grace run --watch`: watches your `./bot` directory for Python file changes and reloads the corresponding extension modules without restarting the bot. + +::: grace.watcher.Watcher + +::: grace.watcher.BotEventHandler diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..137eec5 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,110 @@ +site_name: Grace Framework +site_description: An opinionated, extensible Discord bot framework built on top of discord.py. +repo_url: https://github.com/Code-Society-Lab/grace-framework +repo_name: Code-Society-Lab/grace-framework +theme: + name: material + palette: + - media: (prefers-color-scheme) + toggle: + icon: material/lightbulb-auto + name: Switch to light mode + - media: '(prefers-color-scheme: light)' + scheme: default + primary: black + accent: amber + toggle: + icon: material/lightbulb + name: Switch to dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: black + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to system preference + features: + - content.code.annotate + - content.code.copy + - content.footnote.tooltips + - content.tabs.link + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.path + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + icon: + repo: fontawesome/brands/github-alt + language: en +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: sphinx + show_root_heading: true + show_if_no_docstring: true + inherited_members: false + members_order: source + separate_signature: true + unwrap_annotated: true + filters: + - '!^_' + merge_init_into_class: true + docstring_section_style: spacy + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true +markdown_extensions: + - admonition + - codehilite + - attr_list + - md_in_html + - tables + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.snippets: + base_path: [".."] +nav: + - Home: index.md + - Guides: + - Overview: guides/index.md + - Installation: guides/installation.md + - Creating a Bot: guides/creating-a-bot.md + - Configuration: guides/configuration.md + - Models & Migrations: guides/models.md + - Extensions: guides/extensions.md + - Database Management: guides/database.md + - Writing Generators: guides/generators.md + - Reference: + - Application: reference/application.md + - Bot: reference/bot.md + - CLI: reference/cli.md + - Config: reference/config.md + - Database: reference/database.md + - Exceptions: reference/exceptions.md + - Generator: reference/generator.md + - Importer: reference/importer.md + - Model: reference/model.md + - Watcher: reference/watcher.md + - Examples: + - Overview: examples/index.md + - Task Bot: examples/task-bot.md + - Custom Generator: examples/custom-generator.md + - Scheduled Tasks: examples/scheduler.md + - Hot Reload: examples/hot-reload.md diff --git a/grace/config.py b/grace/config.py index 65d88b8..8ea2575 100644 --- a/grace/config.py +++ b/grace/config.py @@ -63,9 +63,9 @@ class Config: """This class is the application configurations. It loads all the configuration for the given environment - The config environment is chosen by checking the value of the `BOT_ENV` + The config environment is chosen by checking the value of the `GRACE_ENV` environment variable. If the variable is not set it will load - with production by default. + with `development` by default (see `Application.load`). There can be only one config loaded at once. Which means thar if you instantiate a second or multiple Config object, they will all share the diff --git a/grace/watcher.py b/grace/watcher.py index 81960ae..74038e9 100644 --- a/grace/watcher.py +++ b/grace/watcher.py @@ -23,8 +23,8 @@ class Watcher: Wrapper around the watchdog observer that watches a specified directory (./bot) for Python file changes and manages event handling. - :param bot: The bot instance, must implement `on_reload()` and `unload_extension()`. - :type bot: Callable + :param callback: Async, no-argument callback invoked after a reload is handled. + :type callback: ReloadCallback """ def __init__(self, callback: ReloadCallback) -> None: diff --git a/pyproject.toml b/pyproject.toml index 6441609..cad809b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,11 @@ dev = [ "black", "isort", ] +doc = [ + "mkdocs", + "mkdocs-material", + "mkdocstrings[python]", +] [project.urls] "Homepage" = "https://codesociety.xyz" From 6a548a448069d1c8b4b4a0a437664b390a221466 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Fri, 31 Jul 2026 00:02:19 -0400 Subject: [PATCH 2/2] docs: sync with optional-database and grace generate database changes --- docs/docs/examples/task-bot.md | 2 +- docs/docs/guides/configuration.md | 10 ++++++---- docs/docs/guides/creating-a-bot.md | 10 ++++++++-- docs/docs/guides/database.md | 12 ++++++++++++ docs/docs/guides/generators.md | 23 +++++++++++++++++++++++ docs/docs/guides/index.md | 2 +- docs/docs/guides/models.md | 9 ++++++--- docs/docs/index.md | 2 +- docs/docs/reference/cli.md | 13 +++++++++---- 9 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/docs/examples/task-bot.md b/docs/docs/examples/task-bot.md index 0e0c479..570ccfd 100644 --- a/docs/docs/examples/task-bot.md +++ b/docs/docs/examples/task-bot.md @@ -18,7 +18,7 @@ DISCORD_TOKEN=your token here ## 2. Generate the Model ```bash -grace generate model Task name:str description:str done:bool +grace generate model Task name:String description:String done:Boolean grace db up ``` diff --git a/docs/docs/guides/configuration.md b/docs/docs/guides/configuration.md index 39a7d91..fd4bbb9 100644 --- a/docs/docs/guides/configuration.md +++ b/docs/docs/guides/configuration.md @@ -1,13 +1,15 @@ # Configuration -Every generated bot ships with three configuration files under `config/`, all read by [`Config`](../reference/config.md) and combined by [`Application`](../reference/application.md): +Every generated bot ships with configuration files under `config/`, all read by [`Config`](../reference/config.md) and combined by [`Application`](../reference/application.md): | File | Purpose | |---|---| | `config/settings.cfg` | Client identity (name, prefix, description, guild) and the Discord token | -| `config/database.cfg` | Per-environment database connection settings | +| `config/database.cfg` | Per-environment database connection settings — only present if your project has a database | | `config/environment.cfg` | Per-environment logging and SQLAlchemy echo settings | +`config/database.cfg` is optional: it's included by default when you run `grace new`, skipped with `--no-database`, and can be added later with `grace generate database` (see [Database Management](database.md#adding-a-database-later)). Without it, `Config.database`/`database_uri` return `None` and `Application.has_database` is `False`. + Grace uses three environments — `production`, `development`, and `test` — selected via the `GRACE_ENV` environment variable. If unset, `development` is used by default (see `Application.load`). ## Environment Variable Interpolation @@ -46,11 +48,11 @@ url = ${DATABASE_URL} [database.development] adapter = sqlite -database = task_bot_development.db +database = development.db [database.test] adapter = sqlite -database = task_bot_test.db +database = test.db ``` Each section is named `database.`. You need at minimum an `adapter` (the SQL dialect, optionally `dialect+driver`, e.g. `postgresql+psycopg2`) and a `database` name — or a full `url` (as used for `production` above, letting you supply a complete SQLAlchemy connection string via an environment variable). Optional keys: `user`, `password`, `host`, `port`. diff --git a/docs/docs/guides/creating-a-bot.md b/docs/docs/guides/creating-a-bot.md index 5100cc8..db3b68f 100644 --- a/docs/docs/guides/creating-a-bot.md +++ b/docs/docs/guides/creating-a-bot.md @@ -10,6 +10,12 @@ Throughout this guide, we'll create a bot called `task-bot`: grace new task-bot ``` +By default, a database is scaffolded alongside your bot. Pass `--no-database` to skip it — you can always add one later with `grace generate database` (see [Database Management](database.md#adding-a-database-later)): + +```bash +grace new task-bot --no-database +``` + Switch to its directory: ```bash @@ -22,13 +28,13 @@ The generated project includes: | File/Folder | Description | |---|---| -| `alembic.ini` | Alembic configuration file for migrations | +| `alembic.ini` | Alembic configuration file for migrations (only if a database was included) | | `bot/` | Contains your bot logic — primary work area | | `bot/extensions/` | Cogs generated with `grace generate cog` live here | | `bot/models/` | Models generated with `grace generate model` live here | | `bot/helpers/` | Shared helper code for your bot | | `config/` | Configuration files and settings (see [Configuration](configuration.md)) | -| `db/` | Database migrations, seeds, and Alembic environment | +| `db/` | Database migrations, seeds, and Alembic environment (only if a database was included) | | `lib/` | Shared libraries or utilities | | `logs/` | Log files generated by the application | | `README.md` | Project overview and documentation | diff --git a/docs/docs/guides/database.md b/docs/docs/guides/database.md index 6aa0f07..0501a04 100644 --- a/docs/docs/guides/database.md +++ b/docs/docs/guides/database.md @@ -2,6 +2,18 @@ Grace exposes a `grace db` command group for managing your bot's database, plus automatic database creation on startup. +A database is optional. `grace new` scaffolds one by default (`config/database.cfg`, `alembic.ini`, `db/`) — pass `--no-database` to skip it (see [Creating a Bot](creating-a-bot.md#generate-your-bot)). [`Application.has_database`](../reference/application.md) reports whether the current project has one configured; every command below (besides `create`) checks it first and warns instead of failing if it doesn't. + +## Adding a Database Later + +If your project was created with `--no-database`, add one at any time from inside the project directory: + +```bash +grace generate database +``` + +This writes `alembic.ini`, `config/database.cfg`, and a `db/` directory (Alembic environment + `seed.py`) into the current project — the same files `grace new` would have generated. Afterwards, `grace generate model`, `grace generate migration`, and every `grace db` command work normally. + ## Create ```bash diff --git a/docs/docs/guides/generators.md b/docs/docs/guides/generators.md index ea7b148..724b09d 100644 --- a/docs/docs/guides/generators.md +++ b/docs/docs/guides/generators.md @@ -71,6 +71,29 @@ def generator() -> Generator: return CogGenerator() ``` +## Composing Generators + +A generator can call another generator directly. `grace new` does exactly this: [`ProjectGenerator`](../reference/generator.md) scaffolds the project template, then — unless `--no-database` was passed — invokes [`DatabaseGenerator`](../reference/generator.md) itself to add `config/database.cfg`, `alembic.ini`, and `db/`: + +```python +from grace.generators.database_generator import generator as db_generator + + +class ProjectGenerator(Generator): + NAME = "project" + + def generate(self, name: str, database: bool = True): + project_dir = self.generate_template( + self.NAME, + variables={"project_name": name, "database": "yes" if database else "no"}, + ) + + if database: + db_generator().generate(output_dir=project_dir) +``` + +`generate_template` returns the path it just generated into, which is how `DatabaseGenerator` knows where to write its own files. This is also how [`grace generate database`](../guides/database.md#adding-a-database-later) retrofits a database onto an existing `--no-database` project — it's the same `DatabaseGenerator`, just invoked directly from the CLI with the current directory as `output_dir`. + ## Rendering Output `Generator` gives you two ways to produce files, both rooted at `grace/generators/templates/`: diff --git a/docs/docs/guides/index.md b/docs/docs/guides/index.md index cdb4b8c..7e2da54 100644 --- a/docs/docs/guides/index.md +++ b/docs/docs/guides/index.md @@ -5,7 +5,7 @@ Grace Framework is an opinionated, extensible Discord bot framework built on top #### Key Features - Quick to start: generate a full-featured bot in seconds - Modular architecture: clean separation of features via extensions (cogs) -- Database integration: connect your bot to a persistent backend with a single config +- Database integration: opt-in, per-project persistence with a single config - Built-in generators: create extensions, models, and migrations with a single command #### Inspiration diff --git a/docs/docs/guides/models.md b/docs/docs/guides/models.md index bea2b15..b376111 100644 --- a/docs/docs/guides/models.md +++ b/docs/docs/guides/models.md @@ -4,10 +4,10 @@ Models are how your bot talks to its database. Grace models are [`SQLModel`](htt ## Generating a Model -Generate your first model: +Generate your first model, in `column_name:Type` pairs: ```bash -grace generate model Task name:str description:str done:bool +grace generate model Task name:String description:String done:Boolean ``` This command automatically: @@ -27,7 +27,10 @@ Generating /db/alembic/versions/99e6d0cf0aec_create_task.py ... done ``` !!! note - Column definitions are currently limited to basic Python/SQLAlchemy types (`str`, `int`, `float`, `bool`, `String`, `Integer`, `Boolean`, etc.). + Column types are SQLAlchemy-style names, not Python types: `String`, `Text`, `Integer`, `Float`, `Boolean`. Each is mapped to its corresponding Python annotation (`str`, `str`, `int`, `float`, `bool`) in the generated model. Any other type name raises a `ValidationError`. + +!!! note + `grace generate model` requires a database — if your project was created with `--no-database`, it warns and does nothing until you run `grace generate database` (see [Database Management](database.md#adding-a-database-later)). The generated `bot/models/task.py`: diff --git a/docs/docs/index.md b/docs/docs/index.md index 939024f..bd15c48 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -41,7 +41,7 @@ Grace Framework is an opinionated, extensible Discord bot framework built on top - **Quick to start** — generate a full-featured bot in seconds - **Modular architecture** — clean separation of features via extensions (cogs) -- **Database integration** — connect your bot to a persistent backend with SQLModel and Alembic migrations +- **Database integration** — opt-in, per-project persistence backed by SQLModel and Alembic migrations - **Built-in generators** — scaffold extensions, models, and migrations with a single command ## Quickstart diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index faaa0ac..48006d2 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -24,6 +24,10 @@ grace --environment test db up Scaffolds a new bot project named `NAME` in a directory of the same name, via [`grace generate project`](../guides/generators.md). Available outside of a project. +| Option | Default | Description | +|---|---|---| +| `--database / --no-database` | `--database` | Scaffold a database (`config/database.cfg`, `alembic.ini`, `db/`) alongside the project. Skip it and add one later with `grace generate database`. | + ### `grace run` Runs the bot. Automatically creates the database if it doesn't exist. @@ -39,7 +43,7 @@ grace run --watch ### `grace db` -Database management commands — see [Database Management](../guides/database.md) for details on each. +Database management commands — see [Database Management](../guides/database.md) for details on each. All of them (except `create`) require a database — if your project has none, they warn and exit rather than raising. | Command | Arguments | Description | |---|---|---| @@ -56,8 +60,9 @@ Scaffolding commands — see [Writing Generators](../guides/generators.md) for h | Command | Arguments | Description | |---|---|---| | `grace generate cog NAME [DESCRIPTION]` | `NAME` (PascalCase), `DESCRIPTION` (optional) | Generates `bot/extensions/_cog.py`. | -| `grace generate model NAME [COLUMN:TYPE ...]` | `NAME` (PascalCase), variadic `COLUMN:TYPE` pairs | Generates `bot/models/.py` and an initial migration. | -| `grace generate migration MESSAGE` | `MESSAGE` | Autogenerates an Alembic revision from the current model state. | +| `grace generate model NAME [COLUMN:TYPE ...]` | `NAME` (PascalCase), variadic `COLUMN:TYPE` pairs (types: `String`, `Text`, `Integer`, `Float`, `Boolean`) | Generates `bot/models/.py` and an initial migration. Requires a database. | +| `grace generate migration MESSAGE` | `MESSAGE` | Autogenerates an Alembic revision from the current model state. Requires a database. | +| `grace generate database` | — | Adds `config/database.cfg`, `alembic.ini`, and `db/` to the current project (for projects created with `--no-database`). | | `grace generate project NAME` | `NAME` (lowercase/hyphens) | Scaffolds a new project directory (used internally by `grace new`). | -`grace generate` is a dynamic command group — any module under `grace.generators` (or a package you register the same way) exposing a `generator()` function shows up here automatically. +`grace generate` is a dynamic command group — any module under `grace.generators` exposing a `generator()` function shows up here automatically.