22 Commits

Author SHA1 Message Date
94f8ff06a3 👷 Update pre-commit configuration commit messages for clarity 2025-03-09 10:26:06 +01:00
b5b469addb 🧑‍💻 Add pre-commit configuration for code quality checks 2025-03-09 10:22:56 +01:00
253325b060 🎨 Fix .gitignore by adding missing newline at end of file 2025-03-09 10:15:28 +01:00
6e9558891b ♻️ Improve formatting and readability in README.md 2025-03-09 10:15:06 +01:00
9dc223951a ⬆️ Update ruff to version 0.9.10 and starlette to version 0.46.1 2025-03-09 10:14:55 +01:00
ef61e5c3ef ♻️ Simplify App initialization by removing default intents in README 2025-03-09 09:56:36 +01:00
67bfd94e56 ♻️ Remove wildcard import from discord in __init__.py 2025-03-09 09:56:11 +01:00
8533e4b23f ♻️ Remove unused imports and update quality checks to target the current directory 2025-03-08 22:03:34 +01:00
fdf2f8e718 📖 Remove production deployment configurations section from README 2025-03-08 22:02:06 +01:00
fea252daf1 📖 Update installation command in README for correct package name 2025-03-08 22:00:48 +01:00
14f0317a1f 📖 Update installation instructions to include pre-release flag 2025-03-08 21:56:50 +01:00
54e6bb27d8 📄 Add copyright and license information to example scripts 2025-03-08 21:51:08 +01:00
7dde4fd16f 📝 Add examples 2025-03-08 21:27:56 +01:00
bfcb7cc33d 🏷️ Update pyproject.toml to include source directory for type checking 2025-03-08 20:38:32 +01:00
9e2b8e0d52 🎨 Format code 2025-03-08 20:36:48 +01:00
c0783fec69 🐛 Update pyproject.toml and uv.lock for execution environments and package versioning 2025-03-08 20:34:50 +01:00
01f27a8c47 🏷️ Update app.py and quality.yaml for type checking and command adjustments 2025-03-08 20:26:00 +01:00
5ec28355d2 🏷️ Update .gitignore and enhance pyproject.toml with metadata and dependencies 2025-03-08 20:23:23 +01:00
d732ba42ac Refactor interaction processing 2025-03-08 19:53:04 +01:00
0b447838aa :feat_ Add Bot alias for App and include in __all__ exports 2025-03-08 19:52:36 +01:00
0dc27e7d29 🏷️ Rename package from pycord-rest to pycord-rest-bot in uv.lock 2025-03-08 19:52:20 +01:00
14466f91d2 📝 Update installation instructions and clarify API method limitations in README.md 2025-03-08 19:52:02 +01:00
11 changed files with 390 additions and 98 deletions

View File

@@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
check: [format, lint]
check: [format, lint, basedpyright]
include:
- check: format
name: "Format Check"
@@ -28,6 +28,9 @@ jobs:
- check: lint
name: "Lint Check"
command: "uv run ruff check ."
- check: basedpyright
name: "Type Check"
command: "uv run basedpyright ."
name: ${{ matrix.name }}

1
.gitignore vendored
View File

@@ -172,3 +172,4 @@ cython_debug/
.python-version
.idea
_version.py

34
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,34 @@
# Copyright (c) NiceBots
# SPDX-License-Identifier: MIT
ci:
autoupdate_commit_msg: ":construction_worker: pre-commit autoupdate"
autofix_commit_msg: ":art: auto fixes from pre-commit.com hooks"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: \.(po|pot|yml|yaml)$
- id: end-of-file-fixer
exclude: \.(po|pot|yml|yaml)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
args: [--prose-wrap=always, --print-width=88]
exclude: \.(po|pot|yml|yaml)$
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.10
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/bhundven/copywrite # waiting for https://github.com/hashicorp/copywrite/pull/120 to be merged
rev: 937f17f09c46992447dfa8977bb96eda512588c4
hooks:
- id: add-headers

109
README.md
View File

@@ -1,12 +1,19 @@
# Pycord REST
A lightweight wrapper for Discord's HTTP interactions using py-cord and FastAPI. This library enables you to build Discord bots that work exclusively through Discord's HTTP interactions, without requiring a WebSocket gateway connection.
A lightweight wrapper for Discord's HTTP interactions using py-cord and FastAPI. This
library enables you to build Discord bots that work exclusively through Discord's HTTP
interactions, without requiring a WebSocket gateway connection.
## About
Pycord REST allows you to build Discord bots that respond to interactions via HTTP endpoints as described in [Discord's interaction documentation](https://discord.com/developers/docs/interactions/receiving-and-responding) and [interaction overview](https://discord.com/developers/docs/interactions/overview#preparing-for-interactions).
Pycord REST allows you to build Discord bots that respond to interactions via HTTP
endpoints as described in
[Discord's interaction documentation](https://discord.com/developers/docs/interactions/receiving-and-responding)
and
[interaction overview](https://discord.com/developers/docs/interactions/overview#preparing-for-interactions).
This project is built on:
- **FastAPI** - For handling the HTTP server
- **py-cord** - Leveraging Discord command builders and interaction handling
- **uvicorn** - ASGI server implementation
@@ -14,16 +21,19 @@ This project is built on:
## Installation
```bash
pip install pycord-reactive-bot
pip install pycord-rest-bot --prerelease=allow
```
Currently, the package is in pre-release, so you need to use the `--prerelease=allow`
flag to install it.
## Quick Start
```python
from pycord_rest import App
import discord
app = App(intents=discord.Intents.default())
app = App()
@app.slash_command(name="ping", description="Responds with pong!")
async def ping(ctx):
@@ -40,11 +50,33 @@ if __name__ == "__main__":
)
```
For more examples, check out the [examples directory](/examples) which includes:
- Basic slash command setup
- Button interactions
- Modal forms
## How It Works
Under the hood, Pycord REST creates an HTTP server using FastAPI and Uvicorn that:
1. Listens for incoming Discord interaction requests on your specified endpoint
2. Verifies the request signature using your application's public key
3. Routes the interaction to the appropriate command handler
4. Returns the response back to Discord
Unlike traditional Discord bots that maintain a persistent WebSocket connection to
Discord's gateway, HTTP-based bots:
- Only wake up when an interaction is received
- Don't receive real-time events from Discord
## Usage
### Setting up your bot on Discord
1. Create a bot on the [Discord Developer Portal](https://discord.com/developers/applications)
1. Create a bot on the
[Discord Developer Portal](https://discord.com/developers/applications)
2. Enable the "Interactions Endpoint URL" for your application
3. Set the URL to your public endpoint where your bot will receive interactions
4. Copy your public key from the Developer Portal to verify signatures
@@ -58,7 +90,8 @@ if __name__ == "__main__":
### Similarities to py-cord
Syntax is equivalent to py-cord, as it is what is being used under the hood, making it easy to adapt existing bots:
Syntax is equivalent to py-cord, as it is what is being used under the hood, making it
easy to adapt existing bots:
```python
@app.slash_command(name="hello", description="Say hello")
@@ -75,17 +108,22 @@ async def button(ctx):
## Limitations
This library works differently than traditional bots because it does not use Discord's WebSocket gateway:
This library works differently than traditional bots because it does not use Discord's
WebSocket gateway:
- **No Cache**: Since there's no gateway connection, there's no cache of guilds, channels, users, etc.
- **Limited API Methods**: Many standard Discord.py/py-cord methods that rely on cache won't work properly:
- `app.get_channel()`, `app.get_guild()`, `app.get_user()`
- **No Cache**: Since there's no gateway connection, there's no cache of guilds,
channels, users, etc.
- **Limited API Methods**: Many standard py-cord methods that rely on cache won't work
properly:
- `app.get_channel()`, `app.get_guild()`, `app.get_user()`, etc.
- Presence updates
- Voice support
- Member tracking
- **Event-Based Functions**: Only interaction-based events work; message events, etc. don't work
- **Event-Based Functions**: Only interaction-based events work; message events, etc.
don't work
Remember that this is an HTTP-only bot that responds exclusively to interactions triggered by users.
Remember that this is an HTTP-only bot that responds exclusively to interactions
triggered by users.
## Configuration Options
@@ -94,15 +132,28 @@ app.run(
token="YOUR_BOT_TOKEN",
public_key="YOUR_PUBLIC_KEY",
uvicorn_options={
"host": "0.0.0.0",
"port": 8000,
"log_level": "info",
"host": "0.0.0.0", # Listen on all network interfaces
"port": 8000, # Port to listen on
"log_level": "info", # Uvicorn logging level
# Any valid uvicorn server options
},
health=True # Enable /health endpoint
)
```
### Server Configuration
For Discord to reach your bot, you need a publicly accessible HTTPS URL. Options
include:
- Using a VPS with a domain and SSL certificate
- Deploying to a cloud service like Heroku, Railway, or Fly.io
### Health Check
By default, Pycord REST includes a `/health` endpoint that returns a 200 status code.
This endpoint is useful for monitoring services like UptimeRobot or health checks.
## Advanced Usage
### Adding Custom FastAPI Routes
@@ -115,9 +166,32 @@ async def custom_endpoint(request: Request):
return {"message": "This is a custom endpoint"}
```
## Development Workflow
For faster development and testing, you can use tunneling tools to expose your local
development server:
- **ngrok** - Creates a secure tunnel to your localhost
```bash
# Install ngrok
npm install -g ngrok
# Expose your local server
ngrok http 8000
```
- **Cloudflare Tunnel** - Provides a secure connection to your local server
- **localtunnel** - Simple tunnel service for exposing local endpoints
These tools provide temporary URLs that you can use in the Discord Developer Portal
during development, allowing you to test changes quickly without deploying to
production.
## Contributing
Contributions are welcome! This project is in early development, so there might be bugs or unexpected behaviors.
Contributions are welcome! This project is in early development, so there might be bugs
or unexpected behaviors.
1. Fork the repository
2. Create a feature branch
@@ -133,7 +207,8 @@ Contributions are welcome! This project is in early development, so there might
## Disclaimer
This is an early-stage project and may have unexpected behaviors or bugs. Please report any issues you encounter.
This is an early-stage project and may have unexpected behaviors or bugs. Please report
any issues you encounter.
## License

48
examples/basic_bot.py Normal file
View File

@@ -0,0 +1,48 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
"""Basic Discord bot example using Pycord REST.
This is a minimal example showing how to create slash commands.
"""
import os
import discord
from dotenv import load_dotenv
from pycord_rest import App
# Load environment variables from .env file
load_dotenv()
app = App()
# Simple ping command
@app.slash_command(name="ping", description="Responds with pong!")
async def ping(ctx: discord.ApplicationContext) -> None:
await ctx.respond("Pong!")
# Command with parameters
@app.slash_command(name="greet", description="Greets a user")
@discord.option("name", input_type=str, description="The name of the user to greet", required=False)
async def greet(ctx: discord.ApplicationContext, name: str | None = None) -> None:
if name:
await ctx.respond(f"Hello, {name}!")
else:
await ctx.respond(f"Hello, {ctx.author.display_name}!")
# Run the app
if __name__ == "__main__":
app.run(
token=os.environ["DISCORD_TOKEN"],
public_key=os.environ["DISCORD_PUBLIC_KEY"],
uvicorn_options={
"host": "0.0.0.0", # noqa: S104
"port": 8000,
"log_level": "info",
},
)

View File

@@ -0,0 +1,55 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
"""Example demonstrating how to use buttons with Pycord REST."""
import os
from typing import Any
import discord
from dotenv import load_dotenv
from pycord_rest import App
# Load environment variables from .env file
load_dotenv()
app = App()
class MyView(discord.ui.View):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_item(
discord.ui.Button(
style=discord.ButtonStyle.link, label="GitHub", url="https://github.com/Paillat-dev/pycord-rest"
)
)
@discord.ui.button(label="Green", style=discord.ButtonStyle.success)
async def green_button(self, button: "discord.ui.Button[MyView]", interaction: discord.Interaction) -> None:
await interaction.respond("You clicked the green button!", ephemeral=True)
@discord.ui.button(label="Red", style=discord.ButtonStyle.danger)
async def red_button(self, button: "discord.ui.Button[MyView]", interaction: discord.Interaction) -> None:
await interaction.respond("You clicked the red button!", ephemeral=True)
# Create a slash command that shows buttons
@app.slash_command(name="buttons", description="Shows interactive buttons")
async def buttons(ctx: discord.ApplicationContext) -> None:
# Create a view with buttons
view = MyView()
await ctx.respond("Choose a button:", view=view)
if __name__ == "__main__":
app.run(
token=os.environ["DISCORD_TOKEN"],
public_key=os.environ["DISCORD_PUBLIC_KEY"],
uvicorn_options={
"host": "0.0.0.0", # noqa: S104
"port": 8000,
"log_level": "info",
},
)

62
examples/modal_example.py Normal file
View File

@@ -0,0 +1,62 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
"""Example showing how to work with modals in Pycord REST."""
import os
from typing import Any
import discord
from dotenv import load_dotenv
from pycord_rest import App
# Load environment variables from .env file
load_dotenv()
app = App()
class MyModal(discord.ui.Modal):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(
discord.ui.InputText(
label="Name", placeholder="Enter your name", style=discord.InputTextStyle.short, custom_id="name_input"
),
discord.ui.InputText(
label="Feedback",
placeholder="Please provide your feedback here...",
style=discord.InputTextStyle.paragraph,
custom_id="feedback_input",
),
*args,
**kwargs,
)
async def callback(self, interaction: discord.Interaction) -> None:
name = self.children[0].value
await interaction.respond(
f"Thank you for your feedback, {name}! Your submission has been received.", ephemeral=True
)
# Command that shows a form modal
@app.slash_command(name="feedback", description="Submit feedback through a form")
async def feedback(ctx: discord.ApplicationContext) -> None:
# Create a modal
modal = MyModal(title="Feedback Form")
await ctx.send_modal(modal)
await ctx.respond("Opening feedback form...", ephemeral=True)
if __name__ == "__main__":
app.run(
token=os.environ["DISCORD_TOKEN"],
public_key=os.environ["DISCORD_PUBLIC_KEY"],
uvicorn_options={
"host": "0.0.0.0", # noqa: S104
"port": 8000,
"log_level": "info",
},
)

View File

@@ -1,12 +1,21 @@
[project]
name = "pycord-rest-bot"
version = "0.1.0"
dynamic = ["version", "urls"]
description = "A discord rest-bot wrapper for pycord"
readme = "README.md"
authors = [
{ name = "Paillat-dev", email = "me@paillat.dev" }
]
license = "MIT"
requires-python = "==3.12.*"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12"
]
keywords = ["discord", "bot", "rest", "pycord"]
dependencies = [
"fastapi>=0.115.11",
"orjson>=3.10.15",
@@ -15,7 +24,6 @@ dependencies = [
"uvicorn>=0.34.0",
]
[dependency-groups]
dev = [
"basedpyright>=1.28.1",
@@ -24,18 +32,45 @@ dev = [
]
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "src/pycord_rest/_version.py"
[tool.hatch.metadata.hooks.vcs.urls]
Homepage = "https://github.com/Paillat-dev/pycord-rest"
source_archive = "https://github.com/Paillat-dev/pycord-rest/archive/{commit_hash}.zip"
[tool.hatchling]
name = "pycord-rest-bot"
[tool.hatch.build]
include = [
"src/pycord_rest/",
]
exclude = [
".copywrite.hcl",
".github",
".python-version",
"uv.lock",
]
[tool.hatch.build.targets.wheel]
packages = ["src/pycord_rest"]
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "all"
reportUnusedCallResult = false
reportAny = false
executionEnvironments = [
{ root = "src/pycord_rest/_version.py", reportDeprecated = false },
{ root = "examples", reportExplicitAny = false, reportUnknownMemberType = false, reportUnusedParameter = false, reportImplicitOverride = false }
]
[tool.ruff]
target-version = "py312"
@@ -44,20 +79,18 @@ indent-width = 4
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"
exclude = [
"src/pycord_rest/_version.py"
]
[tool.ruff.lint]
select = ["ALL"]
per-file-ignores = {}
per-file-ignores = { "examples/**/*" = ["INP001", "ARG002"] }
extend-ignore = [
"N999",
"D104",
@@ -85,6 +118,3 @@ extend-ignore = [
"PLR0913",
"C901"
]
[tool.uv.workspace]
members = ["test"]

View File

@@ -1,6 +1,7 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
from .app import App
__all__ = ["App"]
Bot = App
__all__ = ["App", "Bot"]

View File

@@ -10,7 +10,6 @@ import aiohttp
import discord
import uvicorn
from discord import Interaction, InteractionType
from discord.ui.view import ViewStore
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from fastapi.exceptions import FastAPIError
from nacl.exceptions import BadSignatureError
@@ -19,34 +18,10 @@ from nacl.signing import VerifyKey
logger = logging.getLogger("pycord.rest")
async def _dispatch_view(view_store: ViewStore, component_type: int, custom_id: str, interaction: Interaction) -> None:
# Code taken from ViewStore.dispatch
view_store._ViewStore__verify_integrity() # noqa: SLF001 # pyright: ignore [reportUnknownMemberType, reportAttributeAccessIssue]
message_id: int | None = interaction.message and interaction.message.id
key = (component_type, message_id, custom_id)
value = view_store._views.get(key) or view_store._views.get( # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType, reportPrivateUsage] # noqa: SLF001
(component_type, None, custom_id)
)
if value is None:
return
view, item = value # pyright: ignore [reportUnknownVariableType]
item.refresh_state(interaction)
# Code taken from View._dispatch_item
if view._View__stopped.done(): # noqa: SLF001 # pyright: ignore [reportAttributeAccessIssue, reportUnknownMemberType]
return
if interaction.message:
view.message = interaction.message
await view._scheduled_task(item, interaction) # noqa: SLF001 # pyright: ignore [reportPrivateUsage, reportUnknownMemberType]
class App(discord.Bot):
def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny]
super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType]
self.app: FastAPI = FastAPI()
self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self.router: APIRouter = APIRouter()
self.public_key: str | None = None
@@ -89,22 +64,31 @@ class App(discord.Bot):
raise HTTPException(status_code=401, detail="Invalid request signature") from e
async def _process_interaction(self, request: Request) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
# Code taken from ConnectionState.parse_interaction_create
data = await request.json()
interaction = Interaction(data=data, state=self._connection)
if data["type"] == 3: # interaction component
custom_id: str = interaction.data["custom_id"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
component_type = interaction.data["component_type"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
await self._dispatch_view(component_type, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType]
if interaction.type == InteractionType.modal_submit:
user_id, custom_id = ( # pyright: ignore [reportUnknownVariableType]
interaction.user.id, # pyright: ignore [reportOptionalMemberAccess]
interaction.data["custom_id"], # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript]
)
await self._connection._modal_store.dispatch(user_id, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType, reportPrivateUsage] # noqa: SLF001
await self.process_application_commands(interaction)
match interaction.type:
case InteractionType.component:
custom_id: str = interaction.data["custom_id"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
component_type = interaction.data["component_type"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
await self._dispatch_view(component_type, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType]
case InteractionType.modal_submit:
user_id, custom_id = ( # pyright: ignore [reportUnknownVariableType]
interaction.user.id, # pyright: ignore [reportOptionalMemberAccess]
interaction.data["custom_id"], # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript]
)
await self._connection._modal_store.dispatch(user_id, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType, reportPrivateUsage] # noqa: SLF001
case InteractionType.ping:
return {"type": 1}
case InteractionType.application_command | InteractionType.auto_complete:
await self.process_application_commands(interaction)
self.dispatch("interaction", interaction)
return {"ok": True}
@override
async def on_interaction(self, *args: Never, **kwargs: Never) -> None:
pass
@override
async def process_application_commands( # noqa: PLR0912
self, interaction: Interaction, auto_sync: bool | None = None
@@ -143,8 +127,8 @@ class App(discord.Bot):
return self._bot.dispatch("unknown_application_command", interaction)
if interaction.type is InteractionType.auto_complete:
self._bot.dispatch("application_command_auto_complete", interaction, command)
await super().on_application_command_auto_complete(interaction, command) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
return self._bot.dispatch("application_command_auto_complete", interaction, command)
return None
ctx = await self.get_application_context(interaction)
@@ -212,7 +196,7 @@ class App(discord.Bot):
self,
token: str,
public_key: str,
uvicorn_options: dict[str, Any] | None = None,
uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True,
) -> None:
await self.login(token)

47
uv.lock generated
View File

@@ -298,8 +298,7 @@ wheels = [
]
[[package]]
name = "pycord-rest"
version = "0.1.0"
name = "pycord-rest-bot"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
@@ -411,27 +410,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.9.9"
version = "0.9.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 }
sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 },
{ url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 },
{ url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 },
{ url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 },
{ url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 },
{ url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 },
{ url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 },
{ url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 },
{ url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 },
{ url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 },
{ url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 },
{ url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 },
{ url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 },
{ url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 },
{ url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 },
{ url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 },
{ url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
{ url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 },
{ url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 },
{ url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 },
{ url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 },
{ url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 },
{ url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 },
{ url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 },
{ url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 },
{ url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 },
{ url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 },
{ url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 },
{ url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 },
{ url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 },
{ url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 },
{ url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 },
{ url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 },
{ url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 },
]
[[package]]
@@ -445,14 +444,14 @@ wheels = [
[[package]]
name = "starlette"
version = "0.46.0"
version = "0.46.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/b6/fb9a32e3c5d59b1e383c357534c63c2d3caa6f25bf3c59dd89d296ecbaec/starlette-0.46.0.tar.gz", hash = "sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50", size = 2575568 }
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 },
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]