vibes-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. vibes_client-0.1.0/.gitignore +42 -0
  2. vibes_client-0.1.0/LICENSE +21 -0
  3. vibes_client-0.1.0/PKG-INFO +442 -0
  4. vibes_client-0.1.0/README.md +415 -0
  5. vibes_client-0.1.0/examples/__init__.py +1 -0
  6. vibes_client-0.1.0/examples/app_commands.py +87 -0
  7. vibes_client-0.1.0/examples/events.py +130 -0
  8. vibes_client-0.1.0/examples/example_extension.py +72 -0
  9. vibes_client-0.1.0/examples/extensions.py +84 -0
  10. vibes_client-0.1.0/examples/quickstart.py +52 -0
  11. vibes_client-0.1.0/examples/rest_api.py +91 -0
  12. vibes_client-0.1.0/examples/text_commands.py +142 -0
  13. vibes_client-0.1.0/pyproject.toml +41 -0
  14. vibes_client-0.1.0/vibes/__init__.py +30 -0
  15. vibes_client-0.1.0/vibes/cache.py +5 -0
  16. vibes_client-0.1.0/vibes/clients.py +741 -0
  17. vibes_client-0.1.0/vibes/commands/__init__.py +49 -0
  18. vibes_client-0.1.0/vibes/commands/app_commands.py +187 -0
  19. vibes_client-0.1.0/vibes/commands/commands.py +149 -0
  20. vibes_client-0.1.0/vibes/commands/context.py +35 -0
  21. vibes_client-0.1.0/vibes/commands/cooldown.py +32 -0
  22. vibes_client-0.1.0/vibes/commands/errors.py +65 -0
  23. vibes_client-0.1.0/vibes/commands/params.py +22 -0
  24. vibes_client-0.1.0/vibes/donations.py +10 -0
  25. vibes_client-0.1.0/vibes/errors.py +15 -0
  26. vibes_client-0.1.0/vibes/events.py +139 -0
  27. vibes_client-0.1.0/vibes/ext/__init__.py +16 -0
  28. vibes_client-0.1.0/vibes/ext/errors.py +25 -0
  29. vibes_client-0.1.0/vibes/ext/listeners.py +51 -0
  30. vibes_client-0.1.0/vibes/messaging.py +62 -0
  31. vibes_client-0.1.0/vibes/models.py +10 -0
  32. vibes_client-0.1.0/vibes/streams.py +65 -0
  33. vibes_client-0.1.0/vibes/users.py +112 -0
@@ -0,0 +1,42 @@
1
+ # Bytecompiled / optimized
2
+ *__pycache__*
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+ .env
17
+
18
+ # IDE
19
+ tempCodeRunnerFile.*
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+
25
+ # Testing / coverage
26
+ tests/
27
+ out.log
28
+ run.bat
29
+ test.py
30
+ .pytest_cache/
31
+ htmlcov/
32
+ .coverage
33
+ .coverage.*
34
+
35
+ # mypy / ruff
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+ build.bat
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 drek124
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,442 @@
1
+ Metadata-Version: 2.5
2
+ Name: vibes-client
3
+ Version: 0.1.0
4
+ Summary: The VIBES Streaming API Wrapper
5
+ Project-URL: Homepage, https://github.com/drek124/vibes
6
+ Project-URL: Repository, https://github.com/drek124/vibes
7
+ Project-URL: Issues, https://github.com/drek124/vibes/issues
8
+ Author-email: drek124 <drek.dev124@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,async,streaming,vibes,wrapper
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Communications :: Chat
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx
25
+ Requires-Dist: websockets
26
+ Description-Content-Type: text/markdown
27
+
28
+ # VIBES Client
29
+
30
+ An async Python wrapper for the [VIBES](https://vibes.stream/) streaming API — REST endpoints, a realtime
31
+ WebSocket client, a typed command framework, and a plugin (extension) system.
32
+
33
+ ```python
34
+ import os
35
+ import asyncio
36
+ import vibes
37
+ from vibes import Client
38
+
39
+ client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")
40
+
41
+ @client.event()
42
+ async def on_message(message: vibes.Message):
43
+ if message.content == "!ping":
44
+ await message.reply("Pong!")
45
+
46
+ asyncio.run(client.start())
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Table of contents
52
+
53
+ - [Features](#features)
54
+ - [Installation](#installation)
55
+ - [Quickstart](#quickstart)
56
+ - [Clients](#clients)
57
+ - [PassiveClient (REST only)](#passiveclient-rest-only)
58
+ - [Client (WebSocket)](#client-websocket)
59
+ - [Events](#events)
60
+ - [Commands](#commands)
61
+ - [Text commands](#text-commands)
62
+ - [Parameters](#parameters)
63
+ - [Roles](#roles)
64
+ - [Cooldowns](#cooldowns)
65
+ - [App commands](#app-commands)
66
+ - [Error handling](#error-handling)
67
+ - [Extensions](#extensions)
68
+ - [Models reference](#models-reference)
69
+ - [Examples](#examples)
70
+ - [License](#license)
71
+
72
+ ## Features
73
+
74
+ - **REST API** — fetch streams, users and your own application with `PassiveClient`.
75
+ - **Realtime events** — a WebSocket connection with automatic reconnects and 30s heartbeats.
76
+ - **Typed commands** — both classic chat (`!command`) and native VIBES *app commands*, with automatic
77
+ argument parsing, role gating and per-viewer/user/chat cooldowns.
78
+ - **Extensions** — load and unload self-contained modules of commands and listeners at runtime.
79
+ - **Caching** — users and messages are cached to cut down on API calls.
80
+
81
+ ## Installation
82
+
83
+ Python 3.11+ is required. Dependencies are `httpx` and `websockets`.
84
+
85
+ ```bash
86
+ pip install vibes-api
87
+ ```
88
+
89
+ ## Quickstart
90
+
91
+ Grab an API token from your [VIBES dashboard](https://vibes.stream/dashboard/plugins/api) and export it
92
+ as `VIBES_TOKEN` (the examples read it from the environment, e.g. with a `.env` file and `python-dotenv`).
93
+
94
+ ```python
95
+ import os
96
+ import asyncio
97
+ import vibes
98
+ from vibes import Client
99
+
100
+ client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")
101
+
102
+ @client.event()
103
+ async def on_connect():
104
+ print(f"Logged in as {client.user}") # ClientUser
105
+ for stream in await client.fetch_streams(limit=10):
106
+ await client.subscribe_to(stream.channel) # listen to this stream
107
+
108
+ @client.event()
109
+ async def on_message(message: vibes.Message):
110
+ if message.content.startswith("!"):
111
+ await client.process_commands(message) # let the command framework run
112
+
113
+ asyncio.run(client.start())
114
+ ```
115
+
116
+ `Client.start()` opens the WebSocket and runs the event loop until the connection closes — it is the
117
+ last thing you await, and it blocks for the lifetime of the bot.
118
+
119
+ ## Clients
120
+
121
+ ### PassiveClient (REST only)
122
+
123
+ No WebSocket, no events — just authenticated API calls. Use it for scripts, cron jobs or dashboards.
124
+
125
+ ```python
126
+ import vibes
127
+
128
+ api = vibes.PassiveClient(token=TOKEN)
129
+ try:
130
+ app = await api.fetch_client()
131
+ print(app.owner, app.rate_limit)
132
+
133
+ streams = await api.fetch_streams(limit=50, category="chit-chat")
134
+ user = await api.fetch_user("some-streamer") # None if not found
135
+ finally:
136
+ await api.close()
137
+ ```
138
+
139
+ | Method | Returns | Notes |
140
+ | --- | --- | --- |
141
+ | `fetch_client()` | `Application` | Your own app: owner, key, rate limit, webhook. |
142
+ | `fetch_streams(*, limit=100, category=None, mature=True)` | `list[Stream]` | `category` accepts a slug or a `StreamCategory`. `mature=True` requests 18+ streams (403 if your account isn't 18+). |
143
+ | `fetch_user(slug)` | `User \| None` | Accepts a username or ID. Results are cached. |
144
+ | `send_request(coro, *args, raise_for_status=True, **kwargs)` | `httpx.Response` | The low-level call every method is built on. |
145
+ | `close()` | `None` | Closes the underlying `httpx.AsyncClient`. |
146
+ | `clear_cache()` | `None` | Empties the user and message caches. |
147
+
148
+ `send_request` gives you rate-limit handling and typed errors for free:
149
+
150
+ - `429` responses are retried automatically after the `Retry-After` header (default 60s).
151
+ - `404` raises `vibes.errors.NotFound`.
152
+ - `403` raises `vibes.errors.Forbidden`.
153
+ - Any other non-OK status raises `httpx.HTTPStatusError`.
154
+
155
+ Pass `raise_for_status=False` to receive the raw response instead.
156
+
157
+ ### Client (WebSocket)
158
+
159
+ `Client` extends `PassiveClient` with the realtime connection, event listeners and the command
160
+ framework.
161
+
162
+ ```python
163
+ client = Client(token=TOKEN, prefix="!")
164
+ ```
165
+
166
+ - `prefix` is required if you use text commands; it can't be empty or contain spaces.
167
+ - `client.user` is a `ClientUser`, available after `on_connect`.
168
+ - `client.is_running` tells you whether the socket is open.
169
+
170
+ | Member | Description |
171
+ | --- | --- |
172
+ | `start()` | Connect and run forever. |
173
+ | `close()` | Disconnect once started (replaces the passive `close()`). |
174
+ | `event(name=None)` | Decorator registering an event listener. |
175
+ | `subscribe_to(user)` / `unsubscribe_from(user)` | Follow a streamer's events. Requires a running client and a live user. |
176
+ | `process_commands(message)` | Parse a `Message` as a command. |
177
+ | `sync_commands()` | Push your app commands to VIBES. Call it in `on_connect`. |
178
+ | `load_extension(name)` / `unload_extension(name)` | See [Extensions](#extensions). |
179
+ | `commands` / `app_commands` | The registered `Command` / `AppCommand` objects. |
180
+
181
+ ## Events
182
+
183
+ Register listeners with `@client.event()`. The event name is taken from the function name
184
+ (`on_connect` → `connected`), or you can pass one explicitly: `@client.event("chat_message")`.
185
+
186
+ ```python
187
+ @client.event()
188
+ async def on_stream_online(stream: vibes.Stream):
189
+ print(f"{stream.channel} is live: {stream.title}")
190
+ ```
191
+
192
+ | Listener | Arguments | Fires when |
193
+ | --- | --- | --- |
194
+ | `on_connect` | — | The socket is connected and `client.user` is populated. |
195
+ | `on_message` | `message: Message` | A chat message is sent. |
196
+ | `on_message_delete` | `message: Message` | A *cached* message is deleted. |
197
+ | `on_viewer_join` | `stream: Stream, viewer: Viewer` | Someone starts watching a subscribed stream. |
198
+ | `on_viewer_leave` | `stream: Stream, viewer: Viewer` | Someone stops watching. |
199
+ | `on_new_follower` | `streamer: User, viewer: User` | A streamer you subscribe to gains a follower. |
200
+ | `on_stream_like` | `stream: Stream, viewer: Viewer` | Someone likes a stream. |
201
+ | `on_stream_online` | `stream: Stream` | A subscribed stream goes live. |
202
+ | `on_stream_offline` | `stream: Stream` | A subscribed stream ends. |
203
+ | `on_donation` | `donation: Donation` | A donation arrives. |
204
+ | `on_command_error` | `error: CommandError \| Exception` | A command fails (see [Error handling](#error-handling)). |
205
+
206
+ Listeners run as independent asyncio tasks — a slow listener doesn't block the others. Exceptions
207
+ raised inside a listener are logged, not propagated.
208
+
209
+ There is also an internal `_debug` event that receives every raw WebSocket frame:
210
+
211
+ ```python
212
+ @client.event()
213
+ async def _debug(raw: dict):
214
+ ...
215
+ ```
216
+
217
+ ## Commands
218
+
219
+ VIBES supports two kinds of commands. Both accept `role` and `cooldown` options.
220
+
221
+ ### Text commands
222
+
223
+ Plain chat messages starting with your prefix. You must wire `process_commands` into `on_message`:
224
+
225
+ ```python
226
+ from vibes import commands
227
+ from vibes.commands import Context
228
+
229
+ @commands.command() # invoked as "!hello Ada"
230
+ async def hello(ctx: Context, name: str = "world"):
231
+ await ctx.message.reply(f"Hello, {name}!")
232
+
233
+ @client.event()
234
+ async def on_message(message: vibes.Message):
235
+ if message.author and not message.author.bot and message.content.startswith(client.prefix):
236
+ await client.process_commands(message)
237
+ ```
238
+
239
+ `ctx` is always the first parameter and is a `Context`:
240
+
241
+ | Attribute | Description |
242
+ | --- | --- |
243
+ | `ctx.message` | The triggering `Message`. Use `await ctx.message.reply(...)` to respond. |
244
+ | `ctx.author` | The `Viewer` who sent the message. |
245
+ | `ctx.stream` / `ctx.chat` | The `Stream` and its `StreamChat` (`await ctx.chat.send(...)`). |
246
+ | `ctx.command` | The `Command` being run. |
247
+ | `ctx.client` | The `Client`. |
248
+ | `ctx.invocation_id` | The invocation ID (message ID for text commands). |
249
+
250
+ ### Parameters
251
+
252
+ Parameter types come from annotations. Text commands accept `str`, `int` and `float`; app commands
253
+ also accept `Viewer`. A parameter with a default is optional, and when a message contains more words
254
+ than the command takes parameters, the leftovers are merged into the last one — so `!hello Ada
255
+ Lovelace` passes the full name as a single string, and `!donate 10 Have a good one` passes
256
+ `message="Have a good one"`.
257
+
258
+ ```python
259
+ @commands.command()
260
+ async def donate(ctx: Context, amount: int, message: str = "No message"):
261
+ ...
262
+ ```
263
+
264
+ If the user writes something that can't be converted (`!donate abc`), the command is skipped and a
265
+ `BadArgument` error is raised.
266
+
267
+ ### Roles
268
+
269
+ Gate a command behind a minimum role — `Role.VIEWER` (default), `Role.MOD` or `Role.STREAMER`.
270
+ Roles are comparable, so the framework checks `ctx.author.role < command.required_role`.
271
+
272
+ ```python
273
+ @commands.command(role=vibes.Role.MOD)
274
+ async def purge(ctx: Context):
275
+ ...
276
+ ```
277
+
278
+ ### Cooldowns
279
+
280
+ `Cooldown(period, max_uses=1, target=Cooldown.VIEWER)` limits how often a command can run.
281
+
282
+ | Target | Cooldown applies to |
283
+ | --- | --- |
284
+ | `Cooldown.VIEWER` | One viewer in one stream chat (default). |
285
+ | `Cooldown.USER` | One user across all streams. |
286
+ | `Cooldown.CHAT` | The entire chat, shared by everybody. |
287
+
288
+ ```python
289
+ @commands.command(cooldown=commands.Cooldown(period=10, max_uses=2, target=commands.Cooldown.USER))
290
+ async def coinflip(ctx: Context):
291
+ ...
292
+ ```
293
+
294
+ ### App commands
295
+
296
+ Native VIBES commands, registered with the API instead of parsed from chat. Use `describe` to give
297
+ each option a description shown in the VIBES UI, and put the command description in the docstring.
298
+ Call `client.sync_commands()` in `on_connect` to publish them.
299
+
300
+ ```python
301
+ @commands.app_command()
302
+ @commands.describe(name="Your name", age="Your age")
303
+ async def intro(ctx: Context, name: str, age: int):
304
+ """Tells everyone who you are."""
305
+ await ctx.message.reply(f"{ctx.author} is {name}, {age} years old!")
306
+ ```
307
+
308
+ App command handlers receive parsed options as **keyword arguments**, and `ctx.invocation_id` is the
309
+ invocation ID supplied by the server.
310
+
311
+ ### Error handling
312
+
313
+ Command errors are never raised out of `process_commands` — they're dispatched to a single
314
+ `on_command_error` listener, which receives either a `CommandError` subclass or an unexpected
315
+ `Exception` from your callback.
316
+
317
+ | Error | Extra attributes |
318
+ | --- | --- |
319
+ | `CommandNotFound` | — |
320
+ | `NoPermission` | — |
321
+ | `BadArgument` | `.parameter` (`CommandParameter`) |
322
+ | `MissingArguments` | — |
323
+ | `OnCooldown` | `.cooldown`, `.available_after` (seconds) |
324
+
325
+ Every `CommandError` carries `.context` (the `Context`).
326
+
327
+ ```python
328
+ from vibes.commands import CommandError, BadArgument, MissingArguments, NoPermission, OnCooldown
329
+
330
+ @client.event()
331
+ async def on_command_error(error: CommandError):
332
+ if not isinstance(error, CommandError):
333
+ logging.exception("Unexpected command error", exc_info=error)
334
+ return
335
+
336
+ ctx = error.context
337
+ if isinstance(error, BadArgument):
338
+ await ctx.message.reply(f"Invalid value for `{error.parameter.name}`.")
339
+ elif isinstance(error, MissingArguments):
340
+ await ctx.message.reply("Missing required argument(s).")
341
+ elif isinstance(error, NoPermission):
342
+ await ctx.message.reply("You don't have permission to do that.")
343
+ elif isinstance(error, OnCooldown):
344
+ await ctx.message.reply(f"Try again in {error.available_after:.1f}s.")
345
+ ```
346
+
347
+ If no `on_command_error` listener is registered, user-input errors are swallowed silently and
348
+ unexpected exceptions are logged.
349
+
350
+ ## Extensions
351
+
352
+ An extension is a module that exposes a `setup(client)` coroutine. Commands and listeners defined
353
+ inside it can be removed cleanly at runtime.
354
+
355
+ `my_extension.py`:
356
+
357
+ ```python
358
+ import vibes
359
+ from vibes import commands
360
+ from vibes.commands import Context
361
+ from vibes.ext import Extension, event_listener
362
+
363
+
364
+ class MyExtension(Extension):
365
+ def __init__(self, client):
366
+ self.client: vibes.Client = client
367
+
368
+ async def ext_load(self):
369
+ print("Extension loaded")
370
+
371
+ async def ext_unload(self):
372
+ print("Extension unloaded")
373
+
374
+ @event_listener() # events inside an extension use this decorator
375
+ async def on_connect(self):
376
+ print("Connected!")
377
+
378
+ @commands.command(role=vibes.Role.MOD)
379
+ async def ping(self, ctx: Context):
380
+ await ctx.message.reply("Pong!")
381
+
382
+
383
+ async def setup(client): # the entry point
384
+ await client.add_ext(MyExtension(client))
385
+ ```
386
+
387
+ `main.py`:
388
+
389
+ ```python
390
+ await client.load_extension("my_extension") # without ".py"
391
+ ...
392
+ await client.unload_extension("my_extension")
393
+ await client.sync_commands() # push removed app commands to VIBES
394
+ ```
395
+
396
+ The extension class must live in an importable module — it can't be defined in `__main__`.
397
+
398
+ `load_extension` raises one of `ExtensionNotFound`, `ExtensionAlreadyLoaded`, `NoEntryPointError`
399
+ (from `vibes.ext.errors`) or `ExtensionFailed`.
400
+
401
+ ## Models reference
402
+
403
+ | Type | Key attributes |
404
+ | --- | --- |
405
+ | `User` | `id`, `username`, `mention`, `avatar_url`, `bio`, `followers`, `created_at`, `url`, `is_live`, `stream`, `premium` |
406
+ | `ClientUser` | Everything on `User`, plus `name`, `visibility`, `scopes`, `channels` |
407
+ | `Viewer` | Everything on `User`, plus `display_name`, `role`, `bot`, `viewer_id` |
408
+ | `Role` | `name`, `level`; predefined as `Role.VIEWER`, `Role.MOD`, `Role.STREAMER` |
409
+ | `Stream` | `id`, `title`, `channel`, `category`, `viewers`, `likes`, `mature`, `thumbnail_url`, `url`, `started_at`, `duration`, `chat` |
410
+ | `StreamChannel` | `username`, `avatar_url`, `url`, `premium` |
411
+ | `StreamCategory` | `slug`, `name` |
412
+ | `StreamChat` | `await chat.send(content)` |
413
+ | `Message` | `id`, `content`, `author`, `streamer`, `stream`, `reference`, `created_at`, `system`, `await message.reply(content)` |
414
+ | `MessageReference` | `id`, `author`, `content` |
415
+ | `Donation` | `streamer`, `user`, `amount`, `amount_usd`, `currency`, `message` |
416
+ | `Application` | `url`, `key`, `owner`, `rate_limit`, `utm_source`, `mature_allowed`, `webhook` |
417
+ | `APIKey` | `name`, `created_at` |
418
+ | `ApplicationOwner` | `username`, `url` |
419
+ | `ClientWebhook` | `url`, `scope`, `active` |
420
+ | `PremiumSubscription` | `name`, `level`; reached via `user.premium` / `channel.premium` |
421
+
422
+ All datetime attributes (`created_at`, `started_at`) are timezone-aware `datetime.datetime` objects.
423
+
424
+ `User` and `Viewer` compare by ID, and `str(user)` gives the username.
425
+
426
+ ## Examples
427
+
428
+ Runnable examples live in [`examples/`](examples/):
429
+
430
+ | File | Shows |
431
+ | --- | --- |
432
+ | [`quickstart.py`](examples/quickstart.py) | The smallest useful bot: connect, subscribe, reply. |
433
+ | [`events.py`](examples/events.py) | Every event listener and the arguments it receives. |
434
+ | [`rest_api.py`](examples/rest_api.py) | `PassiveClient` for REST-only scripts. |
435
+ | [`text_commands.py`](examples/text_commands.py) | Text commands, typed parameters, roles and cooldowns. |
436
+ | [`app_commands.py`](examples/app_commands.py) | Native app commands, `describe` and `sync_commands`. |
437
+ | [`extensions.py`](examples/extensions.py) | Loading and unloading an extension. |
438
+ | [`example_extension.py`](examples/example_extension.py) | The extension itself. |
439
+
440
+ ## License
441
+
442
+ MIT — see [LICENSE](LICENSE).