Unofficial Python SDK for Canny API
Project description
canny-sdk
Unofficial Python SDK for Canny's REST API.
Features
- 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
Installation
pip install canny-sdk
For development:
pip install canny-sdk[dev]
Quick Start
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)
Safety: Read-Only Mode
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)
Async Client
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())
API Resources
Boards
# List all boards
boards = client.boards.list()
# Retrieve a specific board
board = client.boards.retrieve(id="board_id")
Posts
# 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..."
)
Users
# 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")
Comments
# 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)
Votes
# List votes for a post
votes = client.votes.list(post_id="...")
Categories & Tags
# List categories for a board
categories = client.categories.list(board_id="...")
# List tags for a board
tags = client.tags.list(board_id="...")
Companies
# List companies
companies = client.companies.list(search="acme")
Webhook Verification
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 object
Flask Example
from 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"}
Error Handling
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}")
Configuration
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
)
Environment Variables
CANNY_API_KEY: Your Canny API key (used if not provided directly)
Running Tests
# 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/ -v
Note: Integration tests are read-only and do not modify any data.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file canny_sdk-0.1.0a1.tar.gz.
File metadata
- Download URL: canny_sdk-0.1.0a1.tar.gz
- Upload date:
- Size: 23.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c40795e756b00224111ae03ccb97201b5e710d8b525aaeb0190d5ad82f1e9ff9
|
|
| MD5 |
405e6f587973e684781d9c8086a4d357
|
|
| BLAKE2b-256 |
930160a239d3c28a38686a4bb3ef308700955064a34472f42b4a8d761624454f
|
Provenance
The following attestation bundles were made for canny_sdk-0.1.0a1.tar.gz:
Publisher:
publish.yml on AsyncAlchemist/canny-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
canny_sdk-0.1.0a1.tar.gz -
Subject digest:
c40795e756b00224111ae03ccb97201b5e710d8b525aaeb0190d5ad82f1e9ff9 - Sigstore transparency entry: 857030518
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/canny-sdk@37e7cb728fdfe0e42bb37b70ee5b677b81678e27 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@37e7cb728fdfe0e42bb37b70ee5b677b81678e27 -
Trigger Event:
release
-
Statement type:
File details
Details for the file canny_sdk-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: canny_sdk-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e79f505463c7132c0c5e662affb8c74801d8317280b26ec6f174e085e2182014
|
|
| MD5 |
f133c0a0ba4f0717be7df447acaec127
|
|
| BLAKE2b-256 |
c33a99580f406fe820ebbe185ad0143497321729024bb8064b1e3e278f5185cb
|
Provenance
The following attestation bundles were made for canny_sdk-0.1.0a1-py3-none-any.whl:
Publisher:
publish.yml on AsyncAlchemist/canny-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
canny_sdk-0.1.0a1-py3-none-any.whl -
Subject digest:
e79f505463c7132c0c5e662affb8c74801d8317280b26ec6f174e085e2182014 - Sigstore transparency entry: 857030578
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/canny-sdk@37e7cb728fdfe0e42bb37b70ee5b677b81678e27 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@37e7cb728fdfe0e42bb37b70ee5b677b81678e27 -
Trigger Event:
release
-
Statement type: