yapper-bot 0.1.0__py3-none-any.whl

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.
@@ -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,30 @@
1
+ yapperpy/__init__.py,sha256=ZcVzvvVt1tU39hM3oTtj2nrQxWy1tH_7aEjEm6M-BKk,2873
2
+ yapperpy/__main__.py,sha256=iqKmCOGMnFObr5EUI8DdsFqMPizuzyTeDWl-DtN5tzc,1585
3
+ yapperpy/_core.py,sha256=5fFD-bIq7iptmiUbqFKKqblGWmjELZNtapmPiU1NMbs,1736
4
+ yapperpy/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
5
+ yapperpy/aio_bot.py,sha256=jlBGbyBjc6jpT7QE-xMinqWpWfvspIVOUSZKaYLEpEY,6121
6
+ yapperpy/aio_client.py,sha256=nrv2Eir_M5yjqDXYdC9nPbqXyFAo2xCqVKV3iDnFJBQ,15417
7
+ yapperpy/bot.py,sha256=ZIMjhuBOvCAaeiQBNcwCWI-lyERFC0ccLYu-ezi9_6Y,5634
8
+ yapperpy/client.py,sha256=f17xBVro6rGhN7LQRjbOqAJOzUgePG_CGEf22geXGTQ,14618
9
+ yapperpy/cogs.py,sha256=ovvdPrnp5v-LzRGt1AIG-HmxclklvhB_GDeait1S9Cc,10085
10
+ yapperpy/components.py,sha256=S3VnGDQWpZhlSf4z__8kJe3jXMuLI8xnQbQlQZoMrYs,3663
11
+ yapperpy/context.py,sha256=U4xnPEm3A_EqKbUjmTinZmABAM9kft7jCL8u1Vf_AHc,3070
12
+ yapperpy/embeds.py,sha256=QMT_dSEUEzzJ6aMOmqCxcCEX3kF0VPexYPTqurqiJv4,2754
13
+ yapperpy/endpoints.py,sha256=Iodrcot-hXie1nDkBgzevdV4LjuFhtBzuVY2vl0hiBk,21260
14
+ yapperpy/enums.py,sha256=gvuyNP7HkhOsoPuZpp5Q_05NBAqwYfKP0w_zcvTkuAk,973
15
+ yapperpy/errors.py,sha256=wjgt-z3Cm7Jw8KsPl3q1_DsrbjF79fnbx7I8upHuCdA,1973
16
+ yapperpy/event_models.py,sha256=pBoTmCJJOi_2qMiRoNurXEOhSIbrXtnzPEGmTOe8Y1U,7916
17
+ yapperpy/events.py,sha256=kufxwGlGwRQthMdXwX3nfbZC5kB45wcaTdScAMJ19UU,2224
18
+ yapperpy/framework.py,sha256=IdWK6J1-AxXJhW4gSlCcoBcD6jbgu-C7SkCVyyyq3is,5837
19
+ yapperpy/models.py,sha256=qPkj6qG1ignzV74SIIFvMrQMfRhEkn_pOf-JMr-xiXA,23734
20
+ yapperpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ yapperpy/registry.py,sha256=eff1BUxaBbJzNaLnuMhapYhnzXLrQFolPFebV4vEfN0,5844
22
+ yapperpy/retry.py,sha256=UtWBcrjcLMqV9wVOItUOlKr74JKI8xBOKExjqz8KRnM,3170
23
+ yapperpy/_http/__init__.py,sha256=nK0DOlL3nDZ2AB4Efy9r1X8EEFXqyi-8Nb99r4_v7yQ,461
24
+ yapperpy/_http/aio.py,sha256=hQUYjjNS0t4zx37K58_DiYNszYzS64oyGLTQvUjDoc4,6845
25
+ yapperpy/_http/base.py,sha256=PtH2xDh2_jIIxY8lyiVSy_QsnY-oTvK2kep58kUnh4s,2546
26
+ yapperpy/_http/sync.py,sha256=7qUr5U41L0wo0JW7XnrV5OasRrud3r9PEnki0NRoFZM,2994
27
+ yapper_bot-0.1.0.dist-info/METADATA,sha256=vj1MclNpjkRE8UsYtwtnILdksDKUibo7AMcuDYVVDCg,8661
28
+ yapper_bot-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
29
+ yapper_bot-0.1.0.dist-info/licenses/LICENSE,sha256=D-u-pnCVEDkVELns6oG0Z9q2RNgUbg0jUJM9oZbDLDY,1078
30
+ yapper_bot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.
yapperpy/__init__.py ADDED
@@ -0,0 +1,153 @@
1
+ from ._version import __version__
2
+ from .aio_bot import AsyncBot
3
+ from .aio_client import AsyncYapper
4
+ from .bot import Bot
5
+ from .client import Yapper
6
+ from .cogs import Cog, button, command, listener
7
+ from .components import (
8
+ IS_COMPONENTS_V2,
9
+ btn,
10
+ container,
11
+ gallery,
12
+ layout,
13
+ link_button,
14
+ row,
15
+ section,
16
+ separator,
17
+ text,
18
+ thumbnail,
19
+ )
20
+ from .context import InteractionContext, MessageContext
21
+ from .embeds import Embed
22
+ from .enums import BotStatus, ButtonStyle, CallbackType, ChannelType, EventType
23
+ from .errors import (
24
+ YapperAuthError,
25
+ YapperConflictError,
26
+ YapperConnectionError,
27
+ YapperError,
28
+ YapperNotFoundError,
29
+ YapperPermissionError,
30
+ YapperRateLimitError,
31
+ YapperServerError,
32
+ YapperServiceError,
33
+ YapperTimeoutError,
34
+ YapperValidationError,
35
+ )
36
+ from .event_models import (
37
+ EventPayload,
38
+ InteractionPayload,
39
+ MemberJoinPayload,
40
+ MemberLeavePayload,
41
+ MessagePayload,
42
+ ReactionPayload,
43
+ TestPayload,
44
+ VoicePayload,
45
+ )
46
+ from .models import (
47
+ ActionRow,
48
+ Ban,
49
+ BotProfile,
50
+ BulkDeleteResult,
51
+ Button,
52
+ Category,
53
+ Channel,
54
+ ChannelRow,
55
+ Command,
56
+ Event,
57
+ EventPage,
58
+ Image,
59
+ Member,
60
+ MemberDetail,
61
+ Message,
62
+ Ok,
63
+ PinResult,
64
+ Reaction,
65
+ Role,
66
+ Server,
67
+ StatusResult,
68
+ TimeoutResult,
69
+ User,
70
+ VoiceChannel,
71
+ VoiceDisconnectResult,
72
+ VoiceMoveResult,
73
+ )
74
+ from .models import (
75
+ Embed as EmbedModel,
76
+ )
77
+
78
+ __all__ = [
79
+ "IS_COMPONENTS_V2",
80
+ "ActionRow",
81
+ "AsyncBot",
82
+ "AsyncYapper",
83
+ "Ban",
84
+ "Bot",
85
+ "BotProfile",
86
+ "BotStatus",
87
+ "BulkDeleteResult",
88
+ "Button",
89
+ "ButtonStyle",
90
+ "CallbackType",
91
+ "Category",
92
+ "Channel",
93
+ "ChannelRow",
94
+ "ChannelType",
95
+ "Cog",
96
+ "Command",
97
+ "Embed",
98
+ "EmbedModel",
99
+ "Event",
100
+ "EventPage",
101
+ "EventPayload",
102
+ "EventType",
103
+ "Image",
104
+ "InteractionContext",
105
+ "InteractionPayload",
106
+ "Member",
107
+ "MemberDetail",
108
+ "MemberJoinPayload",
109
+ "MemberLeavePayload",
110
+ "Message",
111
+ "MessageContext",
112
+ "MessagePayload",
113
+ "Ok",
114
+ "PinResult",
115
+ "Reaction",
116
+ "ReactionPayload",
117
+ "Role",
118
+ "Server",
119
+ "StatusResult",
120
+ "TestPayload",
121
+ "TimeoutResult",
122
+ "User",
123
+ "VoiceChannel",
124
+ "VoiceDisconnectResult",
125
+ "VoiceMoveResult",
126
+ "VoicePayload",
127
+ "Yapper",
128
+ "YapperAuthError",
129
+ "YapperConflictError",
130
+ "YapperConnectionError",
131
+ "YapperError",
132
+ "YapperNotFoundError",
133
+ "YapperPermissionError",
134
+ "YapperRateLimitError",
135
+ "YapperServerError",
136
+ "YapperServiceError",
137
+ "YapperTimeoutError",
138
+ "YapperValidationError",
139
+ "__version__",
140
+ "btn",
141
+ "button",
142
+ "command",
143
+ "container",
144
+ "gallery",
145
+ "layout",
146
+ "link_button",
147
+ "listener",
148
+ "row",
149
+ "section",
150
+ "separator",
151
+ "text",
152
+ "thumbnail",
153
+ ]
yapperpy/__main__.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sys
5
+
6
+ from .aio_client import AsyncYapper
7
+ from .client import Yapper
8
+ from .errors import YapperError
9
+
10
+
11
+ def show(client: Yapper) -> None:
12
+ profile = client.bot.get()
13
+ print(f"bot: {profile.username} (id {profile.id})")
14
+ if profile.display_name:
15
+ print(f"name: {profile.display_name}")
16
+ if profile.bio:
17
+ print(f"bio: {profile.bio}")
18
+ print(f"servers: {len(client.servers.list())}")
19
+ for server in client.servers.list():
20
+ print(f" - {server.name} ({server.id})")
21
+
22
+
23
+ async def show_async(client: AsyncYapper) -> None:
24
+ profile = await client.bot.get()
25
+ print(f"bot: {profile.username} (id {profile.id})")
26
+ if profile.display_name:
27
+ print(f"name: {profile.display_name}")
28
+ if profile.bio:
29
+ print(f"bio: {profile.bio}")
30
+ servers = await client.servers.list()
31
+ print(f"servers: {len(servers)}")
32
+ for server in servers:
33
+ print(f" - {server.name} ({server.id})")
34
+
35
+
36
+ def main(argv: list[str]) -> int:
37
+ try:
38
+ if "--async" in argv:
39
+
40
+ async def runner() -> None:
41
+ async with AsyncYapper() as client:
42
+ await show_async(client)
43
+
44
+ asyncio.run(runner())
45
+ return 0
46
+ with Yapper() as client:
47
+ show(client)
48
+ except ValueError as exc:
49
+ print(exc)
50
+ return 1
51
+ except YapperError as exc:
52
+ print(f"{type(exc).__name__}: {exc}")
53
+ return 1
54
+ return 0
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(main(sys.argv[1:]))
yapperpy/_core.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Generic, TypeVar
5
+
6
+ from ._http.base import USER_AGENT, RawRequest, RawResponse, build_url
7
+ from .endpoints import Request
8
+ from .errors import YapperError, error_for_status
9
+ from .retry import retry_after_from
10
+
11
+ C = TypeVar("C")
12
+
13
+
14
+ class Resource(Generic[C]):
15
+ def __init__(self, client: C) -> None:
16
+ self._client = client
17
+
18
+
19
+ def prepare(
20
+ request: Request[Any],
21
+ *,
22
+ base_url: str,
23
+ token: str,
24
+ user_agent: str = USER_AGENT,
25
+ ) -> RawRequest:
26
+ headers = {
27
+ "Authorization": f"Bot {token}",
28
+ "User-Agent": user_agent,
29
+ "Accept": "application/json",
30
+ }
31
+ body: bytes | None = None
32
+ if request.raw_body is not None:
33
+ body = request.raw_body
34
+ headers["Content-Type"] = request.content_type or "application/octet-stream"
35
+ elif request.json_body is not None:
36
+ body = json.dumps(request.json_body, separators=(",", ":")).encode("utf-8")
37
+ headers["Content-Type"] = "application/json"
38
+ return RawRequest(
39
+ method=request.method,
40
+ url=build_url(base_url, request.path, request.query),
41
+ headers=headers,
42
+ body=body,
43
+ )
44
+
45
+
46
+ def error_from_response(raw: RawResponse) -> YapperError:
47
+ body: Any
48
+ try:
49
+ body = raw.json()
50
+ except ValueError:
51
+ body = raw.body[:200].decode("utf-8", "replace")
52
+ message = f"the request failed with status {raw.status}"
53
+ if isinstance(body, dict):
54
+ detail = body.get("error") or body.get("message")
55
+ if isinstance(detail, str) and detail:
56
+ message = detail
57
+ wait = retry_after_from(raw.headers, body)
58
+ return error_for_status(raw.status, message, body=body, retry_after=wait)
@@ -0,0 +1,27 @@
1
+ from .base import (
2
+ DEFAULT_BASE_URL,
3
+ DEFAULT_TIMEOUT,
4
+ USER_AGENT,
5
+ AsyncTransport,
6
+ RawRequest,
7
+ RawResponse,
8
+ SyncTransport,
9
+ TransportError,
10
+ TransportTimeout,
11
+ build_url,
12
+ segment,
13
+ )
14
+
15
+ __all__ = [
16
+ "DEFAULT_BASE_URL",
17
+ "DEFAULT_TIMEOUT",
18
+ "USER_AGENT",
19
+ "AsyncTransport",
20
+ "RawRequest",
21
+ "RawResponse",
22
+ "SyncTransport",
23
+ "TransportError",
24
+ "TransportTimeout",
25
+ "build_url",
26
+ "segment",
27
+ ]