Unofficial Python SDK for Canny's REST API.
- Full support for Canny API v1 and v2 endpoints
- Both synchronous and asynchronous clients
- Type hints and Pydantic models for all API objects
- Automatic pagination helpers
- Webhook signature verification
- Read-only mode (default) to prevent accidental data modifications
pip install canny-sdkFor development:
pip install canny-sdk[dev]from canny import CannyClient
# Initialize client (reads CANNY_API_KEY from environment)
client = CannyClient()
# Or with explicit API key
client = CannyClient(api_key="your_api_key")
# List all boards
boards = client.boards.list()
for board in boards.boards:
print(f"{board.name}: {board.post_count} posts")
# List posts with automatic pagination
for post in client.posts.list_all(board_id="..."):
print(post.title)By default, the client operates in read-only mode. This prevents accidental modifications to your production data.
# Default: read_only=True - write operations will raise CannyReadOnlyError
client = CannyClient()
# To enable write operations, explicitly set read_only=False
client = CannyClient(read_only=False)import asyncio
from canny import CannyAsyncClient
async def main():
async with CannyAsyncClient() as client:
# List boards
boards = await client.boards.list()
# Concurrent requests
boards, users = await asyncio.gather(
client.boards.list(),
client.users.list()
)
asyncio.run(main())# List all boards
boards = client.boards.list()
# Retrieve a specific board
board = client.boards.retrieve(id="board_id")# List posts with filtering
posts = client.posts.list(
board_id="...",
status="open",
sort="newest",
limit=20
)
# Iterate over all posts (automatic pagination)
for post in client.posts.list_all(board_id="..."):
print(post.title, post.score)
# Retrieve a specific post
post = client.posts.retrieve(id="post_id")
# Create a post (requires read_only=False)
post_id = client.posts.create(
author_id="user_id",
board_id="board_id",
title="Feature Request",
details="Please add this feature..."
)# List users (v2 cursor-based pagination)
users = client.users.list(limit=100)
# Iterate over all users
for user in client.users.list_all():
print(user.name, user.email)
# Retrieve a user
user = client.users.retrieve(id="user_id")
user = client.users.retrieve(email="user@example.com")# List comments for a post
comments = client.comments.list(post_id="...")
# Iterate over all comments
for comment in client.comments.list_all(post_id="..."):
print(comment.author.name, comment.value)# List votes for a post
votes = client.votes.list(post_id="...")# List categories for a board
categories = client.categories.list(board_id="...")
# List tags for a board
tags = client.tags.list(board_id="...")# List companies
companies = client.companies.list(search="acme")Verify incoming webhooks from Canny:
from canny import verify_webhook, WebhookVerifier
from canny.exceptions import CannyWebhookVerificationError
# Option 1: Standalone function
try:
verify_webhook(
api_key="your_api_key",
nonce=request.headers["Canny-Nonce"],
signature=request.headers["Canny-Signature"],
timestamp=int(request.headers.get("Canny-Timestamp", 0))
)
except CannyWebhookVerificationError as e:
return {"error": str(e)}, 401
# Option 2: Verifier class (reusable)
verifier = WebhookVerifier(api_key="your_api_key")
verifier.verify(nonce, signature, timestamp)
# Parse the event
event = verifier.parse_event(request.json)
print(event.type) # e.g., "post.created"
print(event.object) # The post/comment/vote objectfrom flask import Flask, request
from canny import WebhookVerifier
from canny.exceptions import CannyWebhookVerificationError
app = Flask(__name__)
verifier = WebhookVerifier(api_key="your_api_key")
@app.route("/webhook", methods=["POST"])
def handle_webhook():
try:
verifier.verify(
nonce=request.headers.get("Canny-Nonce"),
signature=request.headers.get("Canny-Signature"),
timestamp=int(request.headers.get("Canny-Timestamp", 0))
)
except CannyWebhookVerificationError as e:
return {"error": str(e)}, 401
event = verifier.parse_event(request.json)
if event.type == "post.created":
handle_new_post(event.object)
elif event.type == "vote.created":
handle_new_vote(event.object)
return {"status": "ok"}from canny import CannyClient
from canny.exceptions import (
CannyAPIError,
CannyAuthenticationError,
CannyNotFoundError,
CannyRateLimitError,
CannyReadOnlyError,
)
client = CannyClient()
try:
post = client.posts.retrieve(id="nonexistent")
except CannyNotFoundError:
print("Post not found")
except CannyAuthenticationError:
print("Invalid API key")
except CannyRateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
except CannyReadOnlyError:
print("Write operation blocked in read-only mode")
except CannyAPIError as e:
print(f"API error: {e.status_code} - {e.message}")from canny import CannyClient
client = CannyClient(
api_key="your_api_key", # Or set CANNY_API_KEY env var
read_only=True, # Default: True (safe mode)
timeout=30.0, # Request timeout in seconds
)CANNY_API_KEY: Your Canny API key (used if not provided directly)
# Unit tests (no API key needed)
pytest tests/ -v --ignore=tests/integration
# Integration tests (requires CANNY_API_KEY)
CANNY_API_KEY=your_key pytest tests/integration/ -vNote: Integration tests are read-only and do not modify any data.
MIT