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.
Files changed (45) hide show
  1. yapper_bot-0.1.0/.gitignore +14 -0
  2. yapper_bot-0.1.0/LICENSE +21 -0
  3. yapper_bot-0.1.0/PKG-INFO +280 -0
  4. yapper_bot-0.1.0/PLAN.md +264 -0
  5. yapper_bot-0.1.0/README.md +252 -0
  6. yapper_bot-0.1.0/examples/async_bot.py +39 -0
  7. yapper_bot-0.1.0/examples/cogs/fun.py +23 -0
  8. yapper_bot-0.1.0/examples/cogs/moderation.py +15 -0
  9. yapper_bot-0.1.0/examples/cogs_bot.py +21 -0
  10. yapper_bot-0.1.0/examples/echo_bot.py +80 -0
  11. yapper_bot-0.1.0/pyproject.toml +69 -0
  12. yapper_bot-0.1.0/src/yapperpy/__init__.py +153 -0
  13. yapper_bot-0.1.0/src/yapperpy/__main__.py +58 -0
  14. yapper_bot-0.1.0/src/yapperpy/_core.py +58 -0
  15. yapper_bot-0.1.0/src/yapperpy/_http/__init__.py +27 -0
  16. yapper_bot-0.1.0/src/yapperpy/_http/aio.py +175 -0
  17. yapper_bot-0.1.0/src/yapperpy/_http/base.py +97 -0
  18. yapper_bot-0.1.0/src/yapperpy/_http/sync.py +83 -0
  19. yapper_bot-0.1.0/src/yapperpy/_version.py +1 -0
  20. yapper_bot-0.1.0/src/yapperpy/aio_bot.py +170 -0
  21. yapper_bot-0.1.0/src/yapperpy/aio_client.py +423 -0
  22. yapper_bot-0.1.0/src/yapperpy/bot.py +163 -0
  23. yapper_bot-0.1.0/src/yapperpy/client.py +421 -0
  24. yapper_bot-0.1.0/src/yapperpy/cogs.py +285 -0
  25. yapper_bot-0.1.0/src/yapperpy/components.py +129 -0
  26. yapper_bot-0.1.0/src/yapperpy/context.py +95 -0
  27. yapper_bot-0.1.0/src/yapperpy/embeds.py +86 -0
  28. yapper_bot-0.1.0/src/yapperpy/endpoints.py +730 -0
  29. yapper_bot-0.1.0/src/yapperpy/enums.py +49 -0
  30. yapper_bot-0.1.0/src/yapperpy/errors.py +79 -0
  31. yapper_bot-0.1.0/src/yapperpy/event_models.py +257 -0
  32. yapper_bot-0.1.0/src/yapperpy/events.py +80 -0
  33. yapper_bot-0.1.0/src/yapperpy/framework.py +165 -0
  34. yapper_bot-0.1.0/src/yapperpy/models.py +795 -0
  35. yapper_bot-0.1.0/src/yapperpy/py.typed +0 -0
  36. yapper_bot-0.1.0/src/yapperpy/registry.py +160 -0
  37. yapper_bot-0.1.0/src/yapperpy/retry.py +96 -0
  38. yapper_bot-0.1.0/tests/conftest.py +80 -0
  39. yapper_bot-0.1.0/tests/test_async.py +174 -0
  40. yapper_bot-0.1.0/tests/test_bot.py +387 -0
  41. yapper_bot-0.1.0/tests/test_client.py +254 -0
  42. yapper_bot-0.1.0/tests/test_components.py +125 -0
  43. yapper_bot-0.1.0/tests/test_endpoints.py +211 -0
  44. yapper_bot-0.1.0/tests/test_models.py +219 -0
  45. yapper_bot-0.1.0/tests/test_transport.py +184 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .env
9
+ pypi.tkn
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
@@ -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
@@ -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.