yapper-bot 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.
- yapper_bot-0.1.0/.gitignore +14 -0
- yapper_bot-0.1.0/LICENSE +21 -0
- yapper_bot-0.1.0/PKG-INFO +280 -0
- yapper_bot-0.1.0/PLAN.md +264 -0
- yapper_bot-0.1.0/README.md +252 -0
- yapper_bot-0.1.0/examples/async_bot.py +39 -0
- yapper_bot-0.1.0/examples/cogs/fun.py +23 -0
- yapper_bot-0.1.0/examples/cogs/moderation.py +15 -0
- yapper_bot-0.1.0/examples/cogs_bot.py +21 -0
- yapper_bot-0.1.0/examples/echo_bot.py +80 -0
- yapper_bot-0.1.0/pyproject.toml +69 -0
- yapper_bot-0.1.0/src/yapperpy/__init__.py +153 -0
- yapper_bot-0.1.0/src/yapperpy/__main__.py +58 -0
- yapper_bot-0.1.0/src/yapperpy/_core.py +58 -0
- yapper_bot-0.1.0/src/yapperpy/_http/__init__.py +27 -0
- yapper_bot-0.1.0/src/yapperpy/_http/aio.py +175 -0
- yapper_bot-0.1.0/src/yapperpy/_http/base.py +97 -0
- yapper_bot-0.1.0/src/yapperpy/_http/sync.py +83 -0
- yapper_bot-0.1.0/src/yapperpy/_version.py +1 -0
- yapper_bot-0.1.0/src/yapperpy/aio_bot.py +170 -0
- yapper_bot-0.1.0/src/yapperpy/aio_client.py +423 -0
- yapper_bot-0.1.0/src/yapperpy/bot.py +163 -0
- yapper_bot-0.1.0/src/yapperpy/client.py +421 -0
- yapper_bot-0.1.0/src/yapperpy/cogs.py +285 -0
- yapper_bot-0.1.0/src/yapperpy/components.py +129 -0
- yapper_bot-0.1.0/src/yapperpy/context.py +95 -0
- yapper_bot-0.1.0/src/yapperpy/embeds.py +86 -0
- yapper_bot-0.1.0/src/yapperpy/endpoints.py +730 -0
- yapper_bot-0.1.0/src/yapperpy/enums.py +49 -0
- yapper_bot-0.1.0/src/yapperpy/errors.py +79 -0
- yapper_bot-0.1.0/src/yapperpy/event_models.py +257 -0
- yapper_bot-0.1.0/src/yapperpy/events.py +80 -0
- yapper_bot-0.1.0/src/yapperpy/framework.py +165 -0
- yapper_bot-0.1.0/src/yapperpy/models.py +795 -0
- yapper_bot-0.1.0/src/yapperpy/py.typed +0 -0
- yapper_bot-0.1.0/src/yapperpy/registry.py +160 -0
- yapper_bot-0.1.0/src/yapperpy/retry.py +96 -0
- yapper_bot-0.1.0/tests/conftest.py +80 -0
- yapper_bot-0.1.0/tests/test_async.py +174 -0
- yapper_bot-0.1.0/tests/test_bot.py +387 -0
- yapper_bot-0.1.0/tests/test_client.py +254 -0
- yapper_bot-0.1.0/tests/test_components.py +125 -0
- yapper_bot-0.1.0/tests/test_endpoints.py +211 -0
- yapper_bot-0.1.0/tests/test_models.py +219 -0
- yapper_bot-0.1.0/tests/test_transport.py +184 -0
yapper_bot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yapperpy contributors
|
|
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,280 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yapper-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bot framework and typed client for the Yapper Bot API
|
|
5
|
+
Project-URL: Homepage, https://www.yapper.website/developers
|
|
6
|
+
Project-URL: Documentation, https://www.yapper.website/developers/api
|
|
7
|
+
Project-URL: Source, https://github.com/yapperpy/yapperpy
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,bot,chat,discord,sdk,yapper
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# yapperpy
|
|
30
|
+
|
|
31
|
+
A Python package for the [Yapper](https://www.yapper.website) Bot API: a full bot framework with a
|
|
32
|
+
typed REST client, sync and async, and **no runtime dependencies**.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install yapper-bot
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The distribution is `yapper-bot`; the import name is `yapperpy`.
|
|
39
|
+
|
|
40
|
+
Requires Python 3.10 or newer.
|
|
41
|
+
|
|
42
|
+
## A bot in ten lines
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import yapperpy
|
|
46
|
+
|
|
47
|
+
bot = yapperpy.Bot() # token from YAPPER_BOT_TOKEN
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@bot.command("ping", "Is the bot alive?")
|
|
51
|
+
def ping(msg):
|
|
52
|
+
msg.reply("pong")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
bot.run()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The same bot with asyncio:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
import yapperpy
|
|
63
|
+
|
|
64
|
+
bot = yapperpy.AsyncBot()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@bot.command("ping", "Is the bot alive?")
|
|
68
|
+
async def ping(msg):
|
|
69
|
+
await msg.reply("pong")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
asyncio.run(bot.run())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A callback may be a plain function or a coroutine function: the async runtime awaits whatever it is
|
|
76
|
+
gets, so one cog works in both.
|
|
77
|
+
|
|
78
|
+
## What a bot can do
|
|
79
|
+
|
|
80
|
+
| Call | What it does |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `@bot.command(name, description)` | Registers a command. People type `/name` or `!name`. |
|
|
83
|
+
| `@bot.on(event)` | Listens for an event: `message`, `command`, `member_join`, `member_leave`, `reaction_add`, `reaction_remove`, `voice_join`, `voice_leave`, `interaction`, `test`, `ready`, `error`. |
|
|
84
|
+
| `@bot.button("vote:")` | Handles clicks on buttons whose `custom_id` starts with `vote:`. A regular expression works too. |
|
|
85
|
+
| `msg.reply(text)` | Answers the message, quoting it. `msg.say(text)` posts in the same channel without a quote. |
|
|
86
|
+
| `msg.args`, `msg.argv` | The rest of the command line, and the same split on spaces. |
|
|
87
|
+
| `i.reply()`, `i.ephemeral()`, `i.update()`, `i.ack()` | Answers a button click: a new message, one only the clicker sees, an edit of the clicked message, or nothing visible. |
|
|
88
|
+
| `bot.add_cog(Cog)` | Groups commands, listeners and buttons in one class. |
|
|
89
|
+
| `bot.load_extensions("cogs")` | Loads every `.py` file in a folder. `bot.unload_extension` and `bot.reload_extension` work while the bot runs. |
|
|
90
|
+
| `bot.send(channel_id, text)` | Posts a message. |
|
|
91
|
+
|
|
92
|
+
## Cogs
|
|
93
|
+
|
|
94
|
+
A cog is a class with commands, listeners and buttons in it:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# cogs/fun.py
|
|
98
|
+
import yapperpy
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Fun(yapperpy.Cog):
|
|
102
|
+
@yapperpy.command("joke", "Tell a joke")
|
|
103
|
+
async def joke(self, msg):
|
|
104
|
+
await msg.reply("I know a UDP joke, but you might not get it")
|
|
105
|
+
|
|
106
|
+
@yapperpy.listener("member_join")
|
|
107
|
+
async def hello(self, ev):
|
|
108
|
+
print(ev.username, "joined")
|
|
109
|
+
|
|
110
|
+
def check(self, msg): # optional: return False to skip this cog
|
|
111
|
+
return True
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# bot.py
|
|
116
|
+
import yapperpy
|
|
117
|
+
|
|
118
|
+
bot = yapperpy.AsyncBot()
|
|
119
|
+
bot.load_extensions("cogs")
|
|
120
|
+
asyncio.run(bot.run())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## The API client
|
|
124
|
+
|
|
125
|
+
The framework is built on a plain API client you can use on its own.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import yapperpy
|
|
129
|
+
|
|
130
|
+
with yapperpy.Yapper("yb_...") as client:
|
|
131
|
+
print(client.bot.get().username)
|
|
132
|
+
for server in client.servers.list():
|
|
133
|
+
print(server.name, server.member_count)
|
|
134
|
+
for channel in client.channels.list(type="text"):
|
|
135
|
+
print(channel.name)
|
|
136
|
+
message = client.messages.send(channel.id, "hello", embeds=[yapperpy.Embed().title("Hi")])
|
|
137
|
+
client.messages.react(channel.id, message.id, "👍")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every call returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`, `Message`,
|
|
141
|
+
`Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`, `VoiceChannel`, `Image`,
|
|
142
|
+
`Embed`, `ActionRow`, `Button`, `Ok`.
|
|
143
|
+
|
|
144
|
+
Namespaces: `client.bot`, `client.events`, `client.servers`, `client.channels`, `client.messages`,
|
|
145
|
+
`client.interactions`, `client.voice`, `client.users`.
|
|
146
|
+
|
|
147
|
+
Anything the API does that this package does not cover yet is one call away:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
client.api("GET", "/servers") # returns plain JSON
|
|
151
|
+
await async_client.api("GET", "/servers")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Events
|
|
155
|
+
|
|
156
|
+
The bot long-polls `GET /events` and hands each event to the right listeners. The payload of a
|
|
157
|
+
message event is a `MessageContext`, which has every field of the message plus `reply`, `say` and
|
|
158
|
+
`author_mention`. Other events give you a typed payload: `MemberJoinPayload`, `MemberLeavePayload`,
|
|
159
|
+
`ReactionPayload`, `InteractionPayload`, `VoicePayload`, `TestPayload`.
|
|
160
|
+
|
|
161
|
+
The last event id is kept in `bot.last_event_id`, so a bot that restarts can pick up where it left
|
|
162
|
+
off. Events are kept by Yapper for one day.
|
|
163
|
+
|
|
164
|
+
## Buttons and embeds
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import yapperpy
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@bot.command("vote", "Pizza or not")
|
|
171
|
+
def vote(msg):
|
|
172
|
+
msg.reply(
|
|
173
|
+
"Pizza tonight?",
|
|
174
|
+
components=[
|
|
175
|
+
yapperpy.row(
|
|
176
|
+
yapperpy.btn("Yes", "vote:yes", "success", emoji="🍕"),
|
|
177
|
+
yapperpy.btn("No", "vote:no", "danger"),
|
|
178
|
+
yapperpy.link_button("Menu", "https://example.com/menu"),
|
|
179
|
+
)
|
|
180
|
+
],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@bot.button("vote:")
|
|
185
|
+
def on_vote(interaction):
|
|
186
|
+
interaction.ephemeral("You voted " + interaction.custom_id[5:])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A Components V2 layout, where the components are the whole message:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
import yapperpy
|
|
193
|
+
|
|
194
|
+
bot.send_layout(
|
|
195
|
+
channel_id,
|
|
196
|
+
[
|
|
197
|
+
yapperpy.container(
|
|
198
|
+
"## Game night",
|
|
199
|
+
yapperpy.section("**Minecraft**", accessory=yapperpy.btn("Vote", "poll:mc", "primary")),
|
|
200
|
+
yapperpy.separator(),
|
|
201
|
+
yapperpy.gallery("https://example.com/a.png"),
|
|
202
|
+
accent="blue",
|
|
203
|
+
)
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Errors
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
YapperError
|
|
212
|
+
YapperValidationError 400
|
|
213
|
+
YapperAuthError 401
|
|
214
|
+
YapperPermissionError 403
|
|
215
|
+
YapperNotFoundError 404
|
|
216
|
+
YapperConflictError 409
|
|
217
|
+
YapperRateLimitError 429 .retry_after
|
|
218
|
+
YapperServiceError 503 .retry_after
|
|
219
|
+
YapperServerError other 5xx
|
|
220
|
+
YapperConnectionError the network failed
|
|
221
|
+
YapperTimeoutError the request timed out
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
A 429 or a 503 with a `Retry-After` header is waited out and retried automatically. A 429 without
|
|
225
|
+
one is raised right away, because waiting does not help. A failing command goes to the `error`
|
|
226
|
+
listeners, or is printed with a traceback when there are none.
|
|
227
|
+
|
|
228
|
+
## Configuration
|
|
229
|
+
|
|
230
|
+
| Setting | How |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| Token | `YAPPER_BOT_TOKEN`, or `Yapper("yb_...")` |
|
|
233
|
+
| Base URL | `Yapper(base_url=...)`, for a test server |
|
|
234
|
+
| Prefixes | `Bot(prefixes=("/", "!"))` |
|
|
235
|
+
| Bot messages | `Bot(ignore_bots=False)` to answer other bots too |
|
|
236
|
+
| Retries | `Yapper(retry=RetryPolicy(attempts=5))` |
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
python -m venv .venv
|
|
242
|
+
.venv\Scripts\python.exe -m pip install --editable ".[dev]"
|
|
243
|
+
.venv\Scripts\python.exe -m pytest
|
|
244
|
+
.venv\Scripts\python.exe -m mypy
|
|
245
|
+
.venv\Scripts\python.exe -m ruff check .
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`start.bat` sets up the environment and runs `examples/echo_bot.py`.
|
|
249
|
+
|
|
250
|
+
## Publishing
|
|
251
|
+
|
|
252
|
+
`publish.bat` builds the wheel and the source archive, checks the metadata and uploads them.
|
|
253
|
+
|
|
254
|
+
```bat
|
|
255
|
+
publish.bat :: upload to PyPI
|
|
256
|
+
publish.bat test :: upload to TestPyPI instead
|
|
257
|
+
publish.bat --dry-run :: build and check everything, do not upload
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The token comes from a file called `pypi.tkn` in this folder. One line, the whole token:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
pypi-AgEIcHlwaS5vcmMFAKEtoken1234567890abcdef
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`pypi.tkn` is in `.gitignore` and is never uploaded. If you would rather not keep a file around,
|
|
267
|
+
set the `PYPI_TOKEN` environment variable instead.
|
|
268
|
+
|
|
269
|
+
The token needs the `pypi` scope for the project `yapper-bot`. The script refuses to upload a
|
|
270
|
+
version that is already on the index, so bump `__version__` in `src/yapperpy/_version.py` first.
|
|
271
|
+
|
|
272
|
+
## Links
|
|
273
|
+
|
|
274
|
+
- API reference: https://www.yapper.website/developers/api
|
|
275
|
+
- OpenAPI file: https://www.yapper.website/openapi.json
|
|
276
|
+
- Developer guide: https://www.yapper.website/developers
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
yapper_bot-0.1.0/PLAN.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# yapperpy - plan
|
|
2
|
+
|
|
3
|
+
A Python package for the Yapper Bot API: a complete bot framework with a typed REST client,
|
|
4
|
+
zero runtime dependencies, sync and async flavours sharing one core.
|
|
5
|
+
|
|
6
|
+
Source of truth: https://www.yapper.website/developers/api and https://www.yapper.website/openapi.json
|
|
7
|
+
(OpenAPI 3.1, 33 paths, 49 operations, 23 schemas). Reference SDK for behaviour parity:
|
|
8
|
+
https://www.yapper.website/sdk/yapper.py
|
|
9
|
+
|
|
10
|
+
## Decisions
|
|
11
|
+
|
|
12
|
+
| Topic | Decision | Why |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Scope | Full bot framework: REST client + commands + events + cogs + extensions | A library, not a script; replaces the single-file SDK with something installable and typed |
|
|
15
|
+
| Sync / async | Both, one shared core | Bot authors use either; the framework detects coroutine callbacks and awaits them in async mode |
|
|
16
|
+
| Dependencies | Zero runtime dependencies (stdlib only) | `pip install yapper-bot` never breaks a user's environment; matches the official SDK's promise |
|
|
17
|
+
| Distribution | `yapper-bot` | `yapper` is taken on PyPI by an unrelated Astro docstring parser, and PyPI rejects `yapperpy` and `py-yapper` as too similar to it |
|
|
18
|
+
| Import name | `yapperpy` | The official SDK's file is literally `yapper.py`; no shadowing, no confusion |
|
|
19
|
+
| Python | 3.10+ | `dataclass(slots=True)`, `X | Y` at runtime, `match` statements |
|
|
20
|
+
| Models | stdlib dataclasses, explicit parsers | No validation dependency; unknown fields ignored for forward compatibility |
|
|
21
|
+
|
|
22
|
+
## Layout
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
py-yapper/
|
|
26
|
+
pyproject.toml hatchling, src layout, no runtime deps
|
|
27
|
+
README.md LICENSE (MIT) PLAN.md
|
|
28
|
+
start.bat runs the example bot (project requirement: runnable entry point)
|
|
29
|
+
examples/
|
|
30
|
+
echo_bot.py minimal bot: command, listener, button, embed
|
|
31
|
+
async_bot.py the same bot on the async runtime
|
|
32
|
+
cogs_bot.py loads a folder of cogs
|
|
33
|
+
cogs/fun.py cogs/moderation.py
|
|
34
|
+
src/yapperpy/
|
|
35
|
+
__init__.py public exports
|
|
36
|
+
__main__.py python -m yapperpy -> prints the bot profile of YAPPER_BOT_TOKEN
|
|
37
|
+
errors.py exception hierarchy
|
|
38
|
+
enums.py ChannelType, BotStatus, ButtonStyle, CallbackType, EventType
|
|
39
|
+
models.py typed models for the 23 schemas
|
|
40
|
+
event_models.py typed payloads for the 9 event types
|
|
41
|
+
endpoints.py Request spec + 52 build_* functions (single source of truth)
|
|
42
|
+
retry.py pure retry policy (status, headers, attempt) -> decision
|
|
43
|
+
client.py Yapper - sync REST client
|
|
44
|
+
aio_client.py AsyncYapper - async REST client
|
|
45
|
+
_core.py request preparation and error mapping
|
|
46
|
+
_http/
|
|
47
|
+
__init__.py
|
|
48
|
+
base.py URL building, header building, body encoding, response parsing
|
|
49
|
+
sync.py SyncTransport: http.client, keep-alive, connection reuse
|
|
50
|
+
aio.py AsyncTransport: asyncio.open_connection, keep-alive
|
|
51
|
+
registry.py command, listener and button registries
|
|
52
|
+
commands.py (folded into registry.py)
|
|
53
|
+
events.py EventStream: long-poll loop with backoff (sync + async)
|
|
54
|
+
cogs.py Cog base class, CogManager, extension loading
|
|
55
|
+
context.py MessageContext and InteractionContext
|
|
56
|
+
framework.py the shared bot base: registration, dispatch helpers
|
|
57
|
+
bot.py Bot: sync runtime
|
|
58
|
+
aio_bot.py AsyncBot: async runtime
|
|
59
|
+
components.py button/row builders + Components V2 layout parts
|
|
60
|
+
embeds.py Embed builder
|
|
61
|
+
py.typed
|
|
62
|
+
tests/
|
|
63
|
+
conftest.py fake transports
|
|
64
|
+
test_models.py test_endpoints.py test_client.py test_async.py
|
|
65
|
+
test_bot.py test_components.py test_transport.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Architecture
|
|
69
|
+
|
|
70
|
+
Five layers. Each layer depends only on the ones above it in this list.
|
|
71
|
+
|
|
72
|
+
**1. Core (pure, no I/O).**
|
|
73
|
+
`endpoints.py` holds one `Request` spec per operation: method, path template, query params, JSON
|
|
74
|
+
body, response model, parser. Every path, body key and response unwrap key exists exactly once,
|
|
75
|
+
here. The request type is generic in the result (`Request[Message]`), so the client methods keep
|
|
76
|
+
real return types instead of `Any`. `models.py` holds the dataclasses and their `from_dict`
|
|
77
|
+
parsers. `retry.py` is a pure function of `(status, headers, body, attempt)` returning
|
|
78
|
+
retry-or-raise plus a delay.
|
|
79
|
+
|
|
80
|
+
**2. Transport.**
|
|
81
|
+
Both transports implement the same interface: build a request, get a `RawResponse`
|
|
82
|
+
(status, headers, body bytes). The sync one uses `http.client.HTTPSConnection` with keep-alive
|
|
83
|
+
and reconnects on a dropped socket. The async one speaks HTTP/1.1 over `asyncio.open_connection`
|
|
84
|
+
with keep-alive. Both are injectable, so tests need no network.
|
|
85
|
+
|
|
86
|
+
**3. Clients.**
|
|
87
|
+
`Yapper` and `AsyncYapper` expose the same namespaces and the same method names. The request
|
|
88
|
+
building, parsing, error mapping and retry loop live in shared helpers; the sync and async
|
|
89
|
+
classes differ only in `await` and `time.sleep` vs `asyncio.sleep`. Method bodies are one-line
|
|
90
|
+
delegations to the shared endpoint builders, which keeps the public types honest
|
|
91
|
+
(`async def send(...) -> Message` really returns a coroutine of `Message`, not `Any`).
|
|
92
|
+
|
|
93
|
+
**4. Framework.**
|
|
94
|
+
Command registry, listener registry, button registry, cog manager, extension loader, event
|
|
95
|
+
stream. The registries are framework-agnostic data structures, shared by both runtimes.
|
|
96
|
+
|
|
97
|
+
**5. Runtime.**
|
|
98
|
+
`Bot` and `AsyncBot` wire the registries to a client and an event stream. A callback that
|
|
99
|
+
returns a coroutine is awaited in async mode and called normally in sync mode, so a cog can be
|
|
100
|
+
written once and used in either runtime.
|
|
101
|
+
|
|
102
|
+
## Public API
|
|
103
|
+
|
|
104
|
+
### Clients
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client = yapperpy.Yapper(token) # or YAPPER_BOT_TOKEN env var
|
|
108
|
+
client = yapperpy.AsyncYapper(token)
|
|
109
|
+
client = yapperpy.Yapper(token, transport=my_transport) # injection point for tests
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Namespaces on both clients:
|
|
113
|
+
|
|
114
|
+
| Namespace | Methods |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `client.bot` | `get`, `update(display_name, bio)`, `set_avatar`, `remove_avatar`, `set_banner`, `remove_banner`, `set_status`, `commands`, `set_commands` |
|
|
117
|
+
| `client.events` | `fetch(after, wait, max_age)`, `stream(...)` |
|
|
118
|
+
| `client.servers` | `list`, `get`, `roles`, `members`, `member_pages`, `member`, `set_nickname`, `add_role`, `remove_role`, `kick`, `bans`, `ban`, `unban`, `timeout` |
|
|
119
|
+
| `client.channels` | `list`, `get`, `create`, `edit`, `delete`, `categories`, `create_category`, `rename_category`, `delete_category` |
|
|
120
|
+
| `client.messages` | `send`, `get`, `edit`, `delete`, `pin`, `unpin`, `react`, `unreact`, `reactions`, `bulk_delete`, `history`, `send_layout`, `edit_layout` |
|
|
121
|
+
| `client.interactions` | `callback(id, token, type, ...)` |
|
|
122
|
+
| `client.voice` | `state`, `move`, `disconnect` |
|
|
123
|
+
| `client.users` | `get`, `dm` |
|
|
124
|
+
|
|
125
|
+
Escape hatch for anything undocumented: `client.api("GET", "/servers")` / `await client.api(...)`.
|
|
126
|
+
|
|
127
|
+
Every method returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`,
|
|
128
|
+
`Message`, `Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`,
|
|
129
|
+
`VoiceChannel`, `Image`, `Embed`, `ActionRow`, `Button`, `LayoutComponent`, `Ok`.
|
|
130
|
+
|
|
131
|
+
### Bot framework
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
bot = yapperpy.Bot() # or yapperpy.AsyncBot()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@bot.command("ping", "Is the bot alive?")
|
|
138
|
+
def ping(msg):
|
|
139
|
+
msg.reply("pong") # msg.args, msg.argv, msg.say(), msg.author_mention
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@bot.on("member_join")
|
|
143
|
+
def welcome(ev):
|
|
144
|
+
print(ev.username, "joined")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@bot.button("vote:") # custom_ids starting with "vote:"
|
|
148
|
+
def vote(i):
|
|
149
|
+
i.ephemeral("You picked " + i.custom_id[5:])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
bot.load_extensions("cogs")
|
|
153
|
+
bot.run()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The async runtime is identical, with `async def` callbacks and `await bot.run()`.
|
|
157
|
+
|
|
158
|
+
Features: slash commands registered with the API (`PUT /commands`), prefix commands
|
|
159
|
+
(`/`, `!` by default, longest prefix wins), `command` / `args` / `argv` parsing, listeners for
|
|
160
|
+
all 9 event types plus `ready` and `error`, button handlers matched by prefix or regex,
|
|
161
|
+
interactions with `reply` / `ephemeral` / `update` / `ack`, cogs as classes or setup functions,
|
|
162
|
+
extensions loaded from a module or a file path with hot reload, Components V2 layouts, embeds.
|
|
163
|
+
|
|
164
|
+
## Error handling
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
YapperError
|
|
168
|
+
YapperValidationError 400
|
|
169
|
+
YapperAuthError 401
|
|
170
|
+
YapperPermissionError 403
|
|
171
|
+
YapperNotFoundError 404
|
|
172
|
+
YapperConflictError 409
|
|
173
|
+
YapperRateLimitError 429 .retry_after
|
|
174
|
+
YapperServiceError 503 .retry_after
|
|
175
|
+
YapperServerError 5xx
|
|
176
|
+
YapperConnectionError network failure
|
|
177
|
+
YapperTimeoutError request timed out
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Every error carries `.status`, `.message` and the parsed body. Rate-limit and service errors
|
|
181
|
+
carry `.retry_after` in seconds.
|
|
182
|
+
|
|
183
|
+
Retry policy: a 429 or 503 with `Retry-After` waits that long and retries (jittered); a 429
|
|
184
|
+
without `Retry-After` fails immediately, because waiting does not help; 502/504 retry with
|
|
185
|
+
exponential backoff; connection failures before the request is sent retry with backoff; 4xx
|
|
186
|
+
never retries. Retries are capped (3 attempts, 60 s ceiling) and configurable.
|
|
187
|
+
|
|
188
|
+
## Models
|
|
189
|
+
|
|
190
|
+
Dataclasses with `slots=True`, explicit `from_dict` parsers, unknown fields ignored.
|
|
191
|
+
Two conveniences the API needs:
|
|
192
|
+
|
|
193
|
+
- `User.avatar_url` and friends are computed from the path the API returns:
|
|
194
|
+
`files.yapper.website/<bucket>/<path>`, or `www.yapper.website<path>` for built-in images
|
|
195
|
+
that start with `/`. `None` when there is no picture.
|
|
196
|
+
- `Channel.type` and `Channel.kind` are the same value; the model exposes one and aliases the other.
|
|
197
|
+
- `GET /channels/{id}/messages` returns authors separately; they are attached to the messages.
|
|
198
|
+
|
|
199
|
+
## Testing and verification
|
|
200
|
+
|
|
201
|
+
- `pytest` for the suite (dev dependency only; the installed package stays dependency-free).
|
|
202
|
+
- Model parsing: one test per schema, including null and missing fields.
|
|
203
|
+
- Endpoint builders: one test per operation asserting method, path, query, body and unwrap key.
|
|
204
|
+
- Transport end-to-end: a local `http.server` on a random port, exercised by both transports,
|
|
205
|
+
covering keep-alive, chunked responses, redirects, error statuses and large bodies.
|
|
206
|
+
- Retry: a fake transport that returns 429/503/5xx in sequence; assert the delays and the
|
|
207
|
+
final exception, and that a 429 without `Retry-After` does not retry.
|
|
208
|
+
- Framework: commands, prefixes, argv, cog add/remove, extension load/unload/reload,
|
|
209
|
+
button prefix and regex matching, interaction answers.
|
|
210
|
+
- Bot runtime: drive a fake event stream and assert the right callbacks fire with the right
|
|
211
|
+
payloads, in both sync and async mode.
|
|
212
|
+
- Static checks: `ruff check`, `ruff format --check`, `mypy --strict`.
|
|
213
|
+
- Packaging: `python -m build` produces a wheel and an sdist; the wheel is installed into a
|
|
214
|
+
throwaway venv and imported to prove it works with no dependencies installed.
|
|
215
|
+
|
|
216
|
+
## Milestones
|
|
217
|
+
|
|
218
|
+
1. Scaffolding: `pyproject.toml`, src layout, `py.typed`, `LICENSE`, `start.bat`.
|
|
219
|
+
2. Core: `errors`, `enums`, `models`, `event_models`, `endpoints`, `retry`.
|
|
220
|
+
3. Transports: `_http/base`, `_http/sync`, `_http/aio`.
|
|
221
|
+
4. Clients: `client`, `aio_client`, pagination helpers, `api()` escape hatch.
|
|
222
|
+
5. Framework: `registry`, `events`, `cogs`, `components`, `embeds`, `context`, `framework`.
|
|
223
|
+
6. Runtime: `bot`, `aio_bot`.
|
|
224
|
+
7. Tests, examples, README, verification pass.
|
|
225
|
+
|
|
226
|
+
All seven are done.
|
|
227
|
+
|
|
228
|
+
## Status
|
|
229
|
+
|
|
230
|
+
Built and verified on 21 October 2026.
|
|
231
|
+
|
|
232
|
+
- 149 tests pass, against the source tree and against the installed wheel.
|
|
233
|
+
- `mypy --strict` is clean over the 25 source files.
|
|
234
|
+
- `ruff check` and `ruff format --check` are clean.
|
|
235
|
+
- The wheel installs into an empty virtualenv with no third-party package beside it, and the
|
|
236
|
+
whole suite passes there too.
|
|
237
|
+
- Every operation in the OpenAPI file has a builder and a client method: 52 builders cover the
|
|
238
|
+
49 documented operations, because two responses are split for convenience (channels and
|
|
239
|
+
categories come from one call) and two Components V2 helpers were added on top.
|
|
240
|
+
|
|
241
|
+
Two design points changed while building:
|
|
242
|
+
|
|
243
|
+
- The bot profile namespace is `client.bot`, not `client.me`, because `me` reads as a value
|
|
244
|
+
rather than a group of calls.
|
|
245
|
+
- The request type is generic in the result (`Request[Message]`), so the public methods return
|
|
246
|
+
real models instead of `Any`. The cost is one type alias per list-shaped response.
|
|
247
|
+
|
|
248
|
+
## Risks and mitigations
|
|
249
|
+
|
|
250
|
+
- Hand-rolled async HTTP: mitigated by testing against a real local server, and by keeping the
|
|
251
|
+
async transport to the subset Yapper uses (Content-Length and chunked bodies, keep-alive).
|
|
252
|
+
- Two clients drifting apart: mitigated by generating both from the same endpoint builders and
|
|
253
|
+
by a test that asserts the two clients expose identical method names and signatures.
|
|
254
|
+
- Zero dependencies means no connection-pooling library: mitigated by implementing keep-alive
|
|
255
|
+
on top of `http.client` and `asyncio` streams, which is enough for a bot's request rate.
|
|
256
|
+
- The API keeps events for one day only: the framework stores the last event id and can be told
|
|
257
|
+
to resume from a saved id after a restart.
|
|
258
|
+
|
|
259
|
+
## Out of scope
|
|
260
|
+
|
|
261
|
+
- OAuth2 "Login with Yapper" (a separate flow with its own client id and secret).
|
|
262
|
+
- Channel webhooks (a different URL scheme and error format).
|
|
263
|
+
- Bot hosting: that runs Node, so it cannot host this package.
|
|
264
|
+
- A voice gateway: bots do not join calls; the API only reports and moves members.
|