python-yappy 0.1.0__py3-none-any.whl → 0.1.2__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.
- {python_yappy-0.1.0.dist-info → python_yappy-0.1.2.dist-info}/METADATA +91 -36
- python_yappy-0.1.2.dist-info/RECORD +12 -0
- yappy/__init__.py +5 -0
- yappy/api.py +45 -12
- yappy/app.py +407 -84
- yappy/core.py +278 -30
- yappy/errors.py +75 -4
- yappy/logger.py +69 -0
- yappy/parser.py +154 -0
- yappy/utils.py +4 -0
- python_yappy-0.1.0.dist-info/RECORD +0 -9
- {python_yappy-0.1.0.dist-info → python_yappy-0.1.2.dist-info}/WHEEL +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-yappy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: API Bindings for Yapper!
|
|
5
5
|
Author: H1387Lmao
|
|
6
6
|
Requires-Dist: aiohttp>=3.14.5
|
|
7
7
|
Requires-Dist: load-dotenv>=0.1.0
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
8
9
|
Requires-Dist: uv-build>=0.13.1
|
|
9
10
|
Requires-Python: >=3.14
|
|
10
11
|
Description-Content-Type: text/markdown
|
|
@@ -24,60 +25,52 @@ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a
|
|
|
24
25
|
- **Commands** — register named commands with descriptions.
|
|
25
26
|
- **Context-based interactions** — reply to messages through a context object.
|
|
26
27
|
- **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
|
|
27
|
-
- **
|
|
28
|
+
- **Argument validation** — type-hinted command parameters are converted for you, and mistakes get a helpful reply.
|
|
29
|
+
- **Rich logging** — coloured logs and readable tracebacks; `verbose=True` adds debug output.
|
|
28
30
|
|
|
29
31
|
## Installation
|
|
30
32
|
|
|
31
|
-
Install YapPy using your project's preferred method.
|
|
32
|
-
|
|
33
|
-
If the package is available on PyPI:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install yappy
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
If you are working from a local checkout, install the project from its repository directory:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pip install .
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
|
|
46
33
|
|
|
47
34
|
```bash
|
|
48
|
-
pip install python-
|
|
35
|
+
pip install python-yappy
|
|
49
36
|
```
|
|
50
37
|
|
|
51
38
|
## Quick start
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
This example uses the dotenv module from `pip install load_dotenv`
|
|
41
|
+
Create a file named `main.py`:
|
|
54
42
|
|
|
55
43
|
```python
|
|
44
|
+
from yappy import App
|
|
56
45
|
import os
|
|
57
|
-
|
|
58
46
|
from dotenv import load_dotenv
|
|
59
|
-
from yappy import App
|
|
60
|
-
|
|
61
47
|
load_dotenv()
|
|
62
|
-
|
|
63
48
|
bot = App(verbose=True)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
@bot.event(
|
|
50
|
+
"message"
|
|
51
|
+
)
|
|
67
52
|
async def on_message(ctx):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await ctx.reply("Hi!")
|
|
72
|
-
|
|
73
|
-
|
|
53
|
+
if ctx.is_bot:
|
|
54
|
+
return
|
|
55
|
+
await bot.process_command(ctx)
|
|
74
56
|
@bot.command(
|
|
75
|
-
|
|
76
|
-
description="replies with 'Pong!'"
|
|
57
|
+
name="ping",
|
|
58
|
+
description="replies with 'Pong!'"
|
|
77
59
|
)
|
|
78
60
|
async def ping(ctx):
|
|
79
|
-
|
|
61
|
+
await ctx.reply("Pong!")
|
|
80
62
|
|
|
63
|
+
@bot.command(
|
|
64
|
+
name="react",
|
|
65
|
+
description="React your message with a certain emoji!"
|
|
66
|
+
)
|
|
67
|
+
async def react(ctx, emoji: str):
|
|
68
|
+
await ctx.message.react(emoji)
|
|
69
|
+
@bot.event(
|
|
70
|
+
"ready"
|
|
71
|
+
)
|
|
72
|
+
async def on_ready():
|
|
73
|
+
print("Logged as", bot.user.name)
|
|
81
74
|
|
|
82
75
|
bot.run(os.getenv("TOKEN"))
|
|
83
76
|
```
|
|
@@ -123,7 +116,7 @@ async def on_message(ctx):
|
|
|
123
116
|
if ctx.is_bot:
|
|
124
117
|
return
|
|
125
118
|
|
|
126
|
-
await
|
|
119
|
+
await bot.process_command(ctx)
|
|
127
120
|
```
|
|
128
121
|
|
|
129
122
|
The event name and handler arguments should match the event interface supported by the installed YapPy version.
|
|
@@ -143,6 +136,66 @@ async def ping(ctx):
|
|
|
143
136
|
|
|
144
137
|
The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
|
|
145
138
|
|
|
139
|
+
### Command arguments
|
|
140
|
+
|
|
141
|
+
Arguments from the message are matched to your function's parameters. Add type hints and YapPy converts them for you:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
@bot.command(name="add", description="Adds two numbers")
|
|
145
|
+
async def add(ctx, a: int, b: int = 0):
|
|
146
|
+
await ctx.reply(f"{a} + {b} = {a + b}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Annotation | Accepts |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `str` | the text exactly as typed (use `"quotes"` for spaces) |
|
|
152
|
+
| `int`, `float` | numbers |
|
|
153
|
+
| `bool` | `true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0` |
|
|
154
|
+
| `User`, `Channel`, `Role` | a mention such as `<@123>` |
|
|
155
|
+
| *(none)* | guessed: whole numbers become `int`, decimals `float`, mentions are resolved, anything else is `str` |
|
|
156
|
+
|
|
157
|
+
Parameters with a default are optional, and `*args` collects any remaining arguments.
|
|
158
|
+
|
|
159
|
+
### Command errors
|
|
160
|
+
|
|
161
|
+
If someone uses a command the wrong way, the bot tells them instead of failing silently:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
!add
|
|
165
|
+
Missing required argument 'a'.
|
|
166
|
+
Usage: `!add <a> [b]`
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Too many arguments and values of the wrong type are handled the same way. Unknown commands are ignored, and an exception raised inside a command is logged rather than sent to chat.
|
|
170
|
+
|
|
171
|
+
To handle these yourself, register a `command_error` handler. It replaces the default replies:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from yappy import UnknownCommand
|
|
175
|
+
|
|
176
|
+
@bot.event("command_error")
|
|
177
|
+
async def on_command_error(ctx, error):
|
|
178
|
+
if isinstance(error, UnknownCommand):
|
|
179
|
+
return
|
|
180
|
+
await ctx.reply(str(error))
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`error` is one of `UnknownCommand`, `MissingArgument`, `TooManyArguments`, `BadArgument` or `CommandInvokeError` (all subclasses of `yappy.CommandError`). For `CommandInvokeError`, the exception your command raised is `error.original`, and `error.command` is the command involved.
|
|
184
|
+
|
|
185
|
+
### Logging
|
|
186
|
+
|
|
187
|
+
YapPy logs through Python's `logging` module under the name `yappy` and prints it with [Rich](https://github.com/Textualize/rich). Exceptions appear as readable tracebacks. `App(verbose=True)` also shows debug output: events, HTTP requests and command invocations.
|
|
188
|
+
|
|
189
|
+
To configure logging yourself, create the app with `configure_logging=False`:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
import logging
|
|
193
|
+
from yappy import App, setup_logging
|
|
194
|
+
|
|
195
|
+
bot = App(configure_logging=False)
|
|
196
|
+
setup_logging(level=logging.WARNING, show_locals=True) # or attach your own handlers to logging.getLogger("yappy")
|
|
197
|
+
```
|
|
198
|
+
|
|
146
199
|
### Context and models
|
|
147
200
|
|
|
148
201
|
YapPy currently provides these core models:
|
|
@@ -153,6 +206,8 @@ YapPy currently provides these core models:
|
|
|
153
206
|
| `Message` | Represents a message. |
|
|
154
207
|
| `Channel` | Represents a channel. |
|
|
155
208
|
| `User` | Represents a user. |
|
|
209
|
+
| `Guild` | Represents a guild / server |
|
|
210
|
+
| `Role` | Represents a role |
|
|
156
211
|
|
|
157
212
|
The example uses the context object (`ctx`) to check `ctx.is_bot` and send a reply with `ctx.reply(...)`. Consult the source code for the currently available attributes and methods of each model.
|
|
158
213
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
yappy/__init__.py,sha256=sfTl69d_NnVtVVLFQbQRmnsTqv8xjC4swCBL-FGxSjc,197
|
|
2
|
+
yappy/api.py,sha256=1j99nfK7n5FBl-vSZy98xtZqh5oBtZZEcFKY4cbHTSs,2854
|
|
3
|
+
yappy/app.py,sha256=-vn22pj5pUBxSLZPiqGZfeJDjmrG44Pu5NTLXQBESec,17300
|
|
4
|
+
yappy/consts.py,sha256=0pIUIY1Ayo6_q3XhWr9hvKLgOT7TC9RgsjPZNVYy8QQ,51
|
|
5
|
+
yappy/core.py,sha256=Z_Yc6cNqN_1qThGVZZ94bpjwMnWHaPlRqhYS3GGRRf0,11104
|
|
6
|
+
yappy/errors.py,sha256=x-CHCU1s4lhZVYqK9EwULYJBbIll8Bt09cC7dZ7Y8oI,2297
|
|
7
|
+
yappy/logger.py,sha256=IVjOwvFtaYLwWGHNurTk7SVcthT8gi02ztuuayBlyIY,2295
|
|
8
|
+
yappy/parser.py,sha256=yOp74Ch8RZ6nAadFVrRxKt5U4VF5rqIClSjWRXmcPAI,5468
|
|
9
|
+
yappy/utils.py,sha256=s7wmPTflqTAIgeAtf0a-gWgEfkQaZGKWYEqm2i4MsUM,129
|
|
10
|
+
python_yappy-0.1.2.dist-info/WHEEL,sha256=lrO5MD1WVAWzcbNy_L2BwtfPrcM3KfUFpbKYXLkJX4A,80
|
|
11
|
+
python_yappy-0.1.2.dist-info/METADATA,sha256=hEgEHzEIx6QaNLAP0uzt1qxaAsYdxaOqKzxXeiDxJMY,7103
|
|
12
|
+
python_yappy-0.1.2.dist-info/RECORD,,
|
yappy/__init__.py
CHANGED
yappy/api.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
from .consts import BASE_URL
|
|
2
2
|
import aiohttp
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
3
9
|
|
|
4
10
|
async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
|
|
5
11
|
headers = {
|
|
@@ -7,10 +13,17 @@ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=
|
|
|
7
13
|
"Content-Type": "application/json"
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
started = time.perf_counter()
|
|
10
17
|
try:
|
|
11
18
|
async with session.request(method, BASE_URL + path, json=body, headers=headers, timeout=40) as r:
|
|
12
19
|
r.raise_for_status()
|
|
13
|
-
|
|
20
|
+
data = await r.json()
|
|
21
|
+
if not path.startswith("/events"): # the long poll would be noise
|
|
22
|
+
log.debug(
|
|
23
|
+
"%s %s -> %s (%.0f ms)", method, path, r.status,
|
|
24
|
+
(time.perf_counter() - started) * 1000
|
|
25
|
+
)
|
|
26
|
+
return data
|
|
14
27
|
except aiohttp.ClientResponseError as e:
|
|
15
28
|
try:
|
|
16
29
|
err = await e.history[-1].json() if e.history else {}
|
|
@@ -23,22 +36,42 @@ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=
|
|
|
23
36
|
wait = int(e.headers.get("Retry-After") or 0) if e.status == 429 or busy else 0
|
|
24
37
|
|
|
25
38
|
if 0 < wait <= 60 and tries > 1:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
delay = wait * (1 + random.random() * 0.5)
|
|
40
|
+
log.warning(
|
|
41
|
+
"%s on %s %s, retrying in %.1fs",
|
|
42
|
+
"Server busy" if busy else "Rate limited", method, path, delay
|
|
43
|
+
)
|
|
44
|
+
await asyncio.sleep(delay)
|
|
45
|
+
return await api_push(session, method, path, body, tries - 1)
|
|
29
46
|
|
|
30
47
|
raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None
|
|
31
|
-
|
|
32
|
-
class
|
|
48
|
+
|
|
49
|
+
class API:
|
|
33
50
|
@staticmethod
|
|
34
|
-
async def
|
|
35
|
-
await api_push(session,
|
|
51
|
+
async def _request(session, method, path="", body=None, tries=3):
|
|
52
|
+
return await api_push(session, method, path, body, tries)
|
|
53
|
+
|
|
36
54
|
@staticmethod
|
|
37
55
|
async def get(session, path="", body=None, tries=3):
|
|
38
|
-
return await
|
|
56
|
+
return await API._request(session, "GET", path, body, tries)
|
|
57
|
+
|
|
39
58
|
@staticmethod
|
|
40
59
|
async def post(session, path="", body=None, tries=3):
|
|
41
|
-
await
|
|
60
|
+
return await API._request(session, "POST", path, body, tries)
|
|
61
|
+
|
|
42
62
|
@staticmethod
|
|
43
|
-
async def
|
|
44
|
-
await
|
|
63
|
+
async def put(session, path="", body=None, tries=3):
|
|
64
|
+
return await API._request(session, "PUT", path, body, tries)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
async def patch(session, path="", body=None, tries=3):
|
|
68
|
+
return await API._request(session, "PATCH", path, body, tries)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
async def delete(session, path="", body=None, tries=3):
|
|
72
|
+
return await API._request(session, "DELETE", path, body, tries)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
async def send(session, channel_id=0, body=None, tries=3):
|
|
76
|
+
return await API._request(session, "POST", f"/channels/{channel_id}/messages", body, tries)
|
|
77
|
+
|