Skip to main content

Unofficial Python SDK for Canny API

Project description

canny-sdk

CI codecov PyPI version Python 3.9+

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

canny_sdk-0.1.0a1.tar.gz (23.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

canny_sdk-0.1.0a1-py3-none-any.whl (38.6 kB view details)

Uploaded Python 3

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

Hashes for canny_sdk-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 c40795e756b00224111ae03ccb97201b5e710d8b525aaeb0190d5ad82f1e9ff9
MD5 405e6f587973e684781d9c8086a4d357
BLAKE2b-256 930160a239d3c28a38686a4bb3ef308700955064a34472f42b4a8d761624454f

See more details on using hashes here.

Provenance

The following attestation bundles were made for canny_sdk-0.1.0a1.tar.gz:

Publisher: publish.yml on AsyncAlchemist/canny-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for canny_sdk-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 e79f505463c7132c0c5e662affb8c74801d8317280b26ec6f174e085e2182014
MD5 f133c0a0ba4f0717be7df447acaec127
BLAKE2b-256 c33a99580f406fe820ebbe185ad0143497321729024bb8064b1e3e278f5185cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for canny_sdk-0.1.0a1-py3-none-any.whl:

Publisher: publish.yml on AsyncAlchemist/canny-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page