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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ venv/
build/
*.egg-info/
uv.lock
.pytest_cache
.ruff_cache


# Dev files
Expand All @@ -17,4 +19,5 @@ test.py
test_async.py
canvas_client.py
.env.local
excluded_specs.toml
testing/
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,85 @@ canvas_api_builder build-all-apis --specs-folder specs/ --output-folder apis/

For more information on using this command run `canvas_api_builder build-all-apis --help`

### Excluding specs from processing

Some Canvas spec files contain malformed parameters that cause code generation to fail or produce invalid Python. You can maintain a local TOML file to exclude these specs from `build-all-apis`, `rebuild-apis`, and `update-spec-files`.

Copy the provided example file to get started:

```bash
cp excluded_specs.toml.example excluded_specs.toml
```

The file format is a single list of spec filenames:

```toml
# excluded_specs.toml
excluded = [
"quiz_extensions.json",
"plagiarism_detection.json",
]
```

Pass it to any command that processes multiple specs via `--exclude-file`:

```bash
canvas_api_builder update-spec-files \
--specs-folder specs/ \
--exclude-file excluded_specs.toml

canvas_api_builder build-all-apis \
--specs-folder specs/ \
--output-folder apis/ \
--exclude-file excluded_specs.toml

canvas_api_builder rebuild-apis \
--specs-folder specs/ \
--apifolder-path apis/ \
--exclude-file excluded_specs.toml
```

>`excluded_specs.toml` is gitignored by default — each user maintains their own list locally.

## Generating LLM documentation

The `canopy_docs` script generates LLM-readable reference files from your project's generated API modules. This is useful for providing context to LLMs when building applications on top of Canopy.

### Generate the Canopy framework reference

```bash
canopy_docs generate-llms
```

Writes a static `llms.txt` describing `CanvasSession`, `CanvasClient`, `CanvasAPIError`, pagination behaviour, async usage, and common usage patterns.

### Generate an index of all API methods

```bash
canopy_docs generate-index --apis-folder apis/
```

Parses your generated API files using AST and emits a compact method index with signatures and inferred return types, grouped by class.

### Generate both in one step

```bash
canopy_docs generate-all --apis-folder apis/
```

### Excluding specs from the index

The same `excluded_specs.toml` file used with `canvas_api_builder` can be passed to `generate-index` and `generate-all` to keep excluded specs out of the generated documentation:

```bash
canopy_docs generate-index \
--apis-folder apis/ \
--exclude-file excluded_specs.toml

canopy_docs generate-all \
--apis-folder apis/ \
--exclude-file excluded_specs.toml
```

## Usage in your project

Expand Down Expand Up @@ -199,7 +278,7 @@ async def main():
tasks = [get_user_details_async(student_id) for student_id in student_ids]
for coro in asyncio.as_completed(tasks):
name = await coro
print(name["name"])
print(name)
print(
f"Total time (asynchronous print as completed): {perf_counter() - time_before}"
)
Expand Down
61 changes: 56 additions & 5 deletions canopy/scripts/canopy_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

import ast
import tomllib
from pathlib import Path

import click
Expand Down Expand Up @@ -283,6 +284,29 @@ async def main():
"""


# ── Excluded specs loader ────────────────────────────────────────────


def load_excluded_specs(exclude_file: Path | None) -> set[str]:
"""Load a set of excluded spec filenames from a TOML file.

The TOML file must contain a top-level ``excluded`` key whose value is a
list of spec filenames (e.g. ``["quiz_extensions.json"]``). Returns an
empty set if *exclude_file* is None.
"""
if exclude_file is None:
return set()
with exclude_file.open("rb") as f:
data = tomllib.load(f)
excluded = data.get("excluded", [])
if not isinstance(excluded, list):
raise click.BadParameter(
f"'excluded' in {exclude_file} must be a list of filenames, "
f"got {type(excluded).__name__}."
)
return set(excluded)


# ── AST helpers ─────────────────────────────────────────────────────


Expand Down Expand Up @@ -363,13 +387,19 @@ def _collect_api_files(
apis_folder: Path,
sync_only: bool,
async_only: bool,
excluded_specs: set[str],
) -> list[Path]:
excluded = {"canvas_client.py", "__init__.py"}
files = sorted(p for p in apis_folder.iterdir() if p.suffix == ".py" and p.name not in excluded)
excluded_py_files = {"canvas_client.py", "__init__.py"}
files = sorted(
p for p in apis_folder.iterdir() if p.suffix == ".py" and p.name not in excluded_py_files
)
if sync_only:
return [p for p in files if "_async" not in p.stem]
files = [p for p in files if "_async" not in p.stem]
if async_only:
return [p for p in files if "_async" in p.stem]
files = [p for p in files if "_async" in p.stem]
if excluded_specs:
# Match against the source spec name (strip _async suffix, swap .py → .json)
files = [p for p in files if f"{p.stem.replace('_async', '')}.json" not in excluded_specs]
return files


Expand Down Expand Up @@ -399,11 +429,19 @@ def cli() -> None:
)
@click.option("--sync-only", is_flag=True, default=False, help="Only index sync modules.")
@click.option("--async-only", is_flag=True, default=False, help="Only index async modules.")
@click.option(
"-e",
"--exclude-file",
default=None,
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
help="TOML file listing spec filenames to exclude from the index.",
)
def generate_index(
apis_folder: Path,
output_file: Path,
sync_only: bool,
async_only: bool,
exclude_file: Path | None,
) -> None:
"""Generate a compact LLM-readable index of all generated API methods.

Expand All @@ -412,7 +450,11 @@ def generate_index(
class. Canopy kwargs (as_user_id, do_not_process, no_data) are collapsed to
'...' since they are identical on every method and documented in llms.txt.
"""
api_files = _collect_api_files(apis_folder, sync_only, async_only)
excluded_specs = load_excluded_specs(exclude_file)
if excluded_specs:
click.echo(f"Excluding {len(excluded_specs)} spec(s): {', '.join(sorted(excluded_specs))}")

api_files = _collect_api_files(apis_folder, sync_only, async_only, excluded_specs)

blocks: list[str] = [
"# Canopy API Index",
Expand Down Expand Up @@ -473,11 +515,19 @@ def generate_llms(output_file: Path) -> None:
type=click.Path(file_okay=False, writable=True, path_type=Path),
help="Folder to write llms.txt and apis_index.txt into.",
)
@click.option(
"-e",
"--exclude-file",
default=None,
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
help="TOML file listing spec filenames to exclude from the index.",
)
@click.pass_context
def generate_all(
ctx: click.Context,
apis_folder: Path,
output_folder: Path,
exclude_file: Path | None,
) -> None:
"""Run generate-llms and generate-index in one step.

Expand All @@ -492,6 +542,7 @@ def generate_all(
output_file=output_folder / "apis_index.txt",
sync_only=False,
async_only=False,
exclude_file=exclude_file,
)


Expand Down
Loading