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.
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-yappy
3
- Version: 0.1.0
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
- - **Optional logging** — enable verbose output when creating the application.
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-dotenv
35
+ pip install python-yappy
49
36
  ```
50
37
 
51
38
  ## Quick start
52
39
 
53
- Create a file named `bot.py`:
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
- @bot.event("message")
49
+ @bot.event(
50
+ "message"
51
+ )
67
52
  async def on_message(ctx):
68
- if ctx.is_bot:
69
- return
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
- name="ping",
76
- description="replies with 'Pong!'",
57
+ name="ping",
58
+ description="replies with 'Pong!'"
77
59
  )
78
60
  async def ping(ctx):
79
- await ctx.reply("Pong!")
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 ctx.reply("Hello!")
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
@@ -1 +1,6 @@
1
1
  from .app import App
2
+ from .errors import (
3
+ BadArgument, CommandError, CommandInvokeError, MissingArgument,
4
+ TooManyArguments, UnknownCommand, YappyError,
5
+ )
6
+ from .logger import setup_logging
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
- return await r.json()
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
- print(f"{'busy' if busy else 'rate limited'}, retrying in {wait}s")
27
- await asyncio.sleep(wait * (1 + random.random() * 0.5))
28
- return await api(session, method, path, body, tries - 1)
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 api:
48
+
49
+ class API:
33
50
  @staticmethod
34
- async def put(session, path="", body=None, tries=3):
35
- await api_push(session, "PUT", path, body, tries)
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 api_push(session, "GET", path, body, tries)
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 api_push(session, "POST", path, body, tries)
60
+ return await API._request(session, "POST", path, body, tries)
61
+
42
62
  @staticmethod
43
- async def send(session, channel_id=0, data=None, tries=3):
44
- await api_push(session, "POST", f"/channels/{channel_id}/messages", data, tries)
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
+