Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ venv.bak/

# mkdocs documentation
/site
docs/site/

# mypy
.mypy_cache/
Expand Down
76 changes: 76 additions & 0 deletions docs/docs/examples/custom-generator.md
Original file line number Diff line number Diff line change
@@ -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 <name>` 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.
33 changes: 33 additions & 0 deletions docs/docs/examples/hot-reload.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions docs/docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -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
```
46 changes: 46 additions & 0 deletions docs/docs/examples/scheduler.md
Original file line number Diff line number Diff line change
@@ -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).
116 changes: 116 additions & 0 deletions docs/docs/examples/task-bot.md
Original file line number Diff line number Diff line change
@@ -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:String description:String done:Boolean
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.
Loading
Loading