6 Commits

4 changed files with 50 additions and 22 deletions

View File

@@ -1,8 +1,18 @@
# Pycord REST <div align="center">
<h1>Pycord REST</h1>
![PyPI - Version](https://img.shields.io/pypi/v/pycord-rest-bot)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycord-rest-bot)
![PyPI - Types](https://img.shields.io/pypi/types/pycord-rest-bot)
![PyPI - License](https://img.shields.io/pypi/l/pycord-rest-bot)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Paillat-dev/pycord-rest/CI.yaml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Paillat-dev/pycord-rest/main.svg)](https://results.pre-commit.ci/latest/github/Paillat-dev/pycord-rest/main)
A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord
and FastAPI. and FastAPI.
</div>
## Table of Contents ## Table of Contents
- [Overview](#overview) - [Overview](#overview)
@@ -14,6 +24,7 @@ and FastAPI.
- [Features](#features) - [Features](#features)
- [Interaction Handling](#interaction-handling) - [Interaction Handling](#interaction-handling)
- [Webhook Events](#webhook-events) - [Webhook Events](#webhook-events)
- [Type Safety](#type-safety)
- [Usage Examples](#usage-examples) - [Usage Examples](#usage-examples)
- [Basic Commands](#basic-commands) - [Basic Commands](#basic-commands)
- [Event Handling](#event-handling) - [Event Handling](#event-handling)
@@ -124,6 +135,15 @@ Handle Discord webhook events such as:
user user
- **Entitlement creation** - When a user subscribes to your app's premium features - **Entitlement creation** - When a user subscribes to your app's premium features
### Type Safety
Pycord REST is fully type-annotated and type-safe. It uses `basedpyright` for type
checking.
<!-- prettier-ignore -->
> [!NOTE]
> While Pycord REST itself is fully typed, the underlying py-cord library has limited type annotations, which may affect type checking in some areas.
## Usage Examples ## Usage Examples
<!-- prettier-ignore --> <!-- prettier-ignore -->
@@ -243,6 +263,7 @@ These tools provide temporary URLs for testing without deploying to production.
**Development Tools**: **Development Tools**:
- **uv**: For dependency management
- **Ruff**: For linting and formatting - **Ruff**: For linting and formatting
- **HashiCorp Copywrite**: For managing license headers - **HashiCorp Copywrite**: For managing license headers
- **basedpyright**: For type checking - **basedpyright**: For type checking

View File

@@ -1,12 +1,8 @@
# Copyright (c) Paillat-dev # Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
"""Basic Discord bot example using Pycord REST. """Example showing how to work with webhook events in Pycord REST."""
This is a minimal example showing how to create slash commands.
"""
import logging
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -16,10 +12,6 @@ from pycord_rest import App, ApplicationAuthorizedEvent
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
# Set up logging
logging.basicConfig(level=logging.DEBUG)
app = App() app = App()
@@ -31,7 +23,7 @@ async def on_application_authorized(event: ApplicationAuthorizedEvent) -> None:
else: else:
print( print(
f"Bot {event.user.display_name} ({event.user.id}) installed the application" # noqa: ISC003 f"Bot {event.user.display_name} ({event.user.id}) installed the application"
+ f" to guild {event.guild.name} ({event.guild.id})." + f" to guild {event.guild.name} ({event.guild.id})."
) )

View File

@@ -17,7 +17,9 @@ classifiers = [
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12" "Programming Language :: Python :: 3.12",
"Typing :: Typed",
"Operating System :: OS Independent",
] ]
keywords = ["discord", "bot", "rest", "pycord"] keywords = ["discord", "bot", "rest", "pycord"]
dependencies = [ dependencies = [
@@ -116,5 +118,6 @@ extend-ignore = [
"FBT002", "FBT002",
"PLR2004", "PLR2004",
"PLR0913", "PLR0913",
"C901" "C901",
"ISC003" # conflicts with basedpyright reportImplicitStringConcatenation
] ]

View File

@@ -11,7 +11,6 @@ import discord
import uvicorn import uvicorn
from discord import Entitlement, Interaction, InteractionType from discord import Entitlement, Interaction, InteractionType
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Response from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import FastAPIError
from nacl.exceptions import BadSignatureError from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey from nacl.signing import VerifyKey
@@ -35,18 +34,26 @@ class ApplicationAuthorizedEvent:
) )
class PycordRestError(discord.DiscordException):
pass
class InvalidCredentialsError(PycordRestError):
pass
class App(discord.Bot): class App(discord.Bot):
def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny] def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny]
super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType] super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType]
self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) self._app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self.router: APIRouter = APIRouter() self.router: APIRouter = APIRouter()
self.public_key: str | None = None self._public_key: str | None = None
@cached_property @cached_property
def _verify_key(self) -> VerifyKey: def _verify_key(self) -> VerifyKey:
if self.public_key is None: if self._public_key is None:
raise FastAPIError("No public key provided") raise InvalidCredentialsError("No public key provided")
return VerifyKey(bytes.fromhex(self.public_key)) return VerifyKey(bytes.fromhex(self._public_key))
async def _dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None: async def _dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
# Code taken from ViewStore.dispatch # Code taken from ViewStore.dispatch
@@ -110,6 +117,7 @@ class App(discord.Bot):
async def process_application_commands( # noqa: PLR0912 async def process_application_commands( # noqa: PLR0912
self, interaction: Interaction, auto_sync: bool | None = None self, interaction: Interaction, auto_sync: bool | None = None
) -> None: ) -> None:
# Code taken from super().process_application_commands
if auto_sync is None: if auto_sync is None:
auto_sync = self._bot.auto_sync_commands # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType] auto_sync = self._bot.auto_sync_commands # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType]
# TODO: find out why the isinstance check below doesn't stop the type errors below # noqa: FIX002, TD002, TD003 # TODO: find out why the isinstance check below doesn't stop the type errors below # noqa: FIX002, TD002, TD003
@@ -231,15 +239,15 @@ class App(discord.Bot):
uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny] uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True, health: bool = True,
) -> None: ) -> None:
self.public_key = public_key self._public_key = public_key
_ = self._process_interaction_factory() _ = self._process_interaction_factory()
_ = self._webhook_event_factory() _ = self._webhook_event_factory()
if health: if health:
_ = self._health_factory() _ = self._health_factory()
self.app.include_router(self.router) self._app.include_router(self.router)
uvicorn_options = uvicorn_options or {} uvicorn_options = uvicorn_options or {}
uvicorn_options["log_level"] = uvicorn_options.get("log_level", logging.root.level) uvicorn_options["log_level"] = uvicorn_options.get("log_level", logging.root.level)
config = uvicorn.Config(self.app, **uvicorn_options) config = uvicorn.Config(self._app, **uvicorn_options)
server = uvicorn.Server(config) server = uvicorn.Server(config)
try: try:
self.dispatch("connect") self.dispatch("connect")
@@ -260,6 +268,10 @@ class App(discord.Bot):
uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny] uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True, health: bool = True,
) -> None: ) -> None:
if not token:
raise InvalidCredentialsError("No token provided")
if not public_key:
raise InvalidCredentialsError("No public key provided")
await self.login(token) await self.login(token)
await self.connect( await self.connect(
token=token, token=token,