python-yappy 0.1.1__tar.gz → 0.1.2__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.
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-yappy
3
- Version: 0.1.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,7 +25,8 @@ 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
 
@@ -114,7 +116,7 @@ async def on_message(ctx):
114
116
  if ctx.is_bot:
115
117
  return
116
118
 
117
- await ctx.process.command(ctx)
119
+ await bot.process_command(ctx)
118
120
  ```
119
121
 
120
122
  The event name and handler arguments should match the event interface supported by the installed YapPy version.
@@ -134,6 +136,66 @@ async def ping(ctx):
134
136
 
135
137
  The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
136
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
+
137
199
  ### Context and models
138
200
 
139
201
  YapPy currently provides these core models:
@@ -13,7 +13,8 @@ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a
13
13
  - **Commands** — register named commands with descriptions.
14
14
  - **Context-based interactions** — reply to messages through a context object.
15
15
  - **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
16
- - **Optional logging** — enable verbose output when creating the application.
16
+ - **Argument validation** — type-hinted command parameters are converted for you, and mistakes get a helpful reply.
17
+ - **Rich logging** — coloured logs and readable tracebacks; `verbose=True` adds debug output.
17
18
 
18
19
  ## Installation
19
20
 
@@ -103,7 +104,7 @@ async def on_message(ctx):
103
104
  if ctx.is_bot:
104
105
  return
105
106
 
106
- await ctx.process.command(ctx)
107
+ await bot.process_command(ctx)
107
108
  ```
108
109
 
109
110
  The event name and handler arguments should match the event interface supported by the installed YapPy version.
@@ -123,6 +124,66 @@ async def ping(ctx):
123
124
 
124
125
  The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
125
126
 
127
+ ### Command arguments
128
+
129
+ Arguments from the message are matched to your function's parameters. Add type hints and YapPy converts them for you:
130
+
131
+ ```python
132
+ @bot.command(name="add", description="Adds two numbers")
133
+ async def add(ctx, a: int, b: int = 0):
134
+ await ctx.reply(f"{a} + {b} = {a + b}")
135
+ ```
136
+
137
+ | Annotation | Accepts |
138
+ | --- | --- |
139
+ | `str` | the text exactly as typed (use `"quotes"` for spaces) |
140
+ | `int`, `float` | numbers |
141
+ | `bool` | `true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0` |
142
+ | `User`, `Channel`, `Role` | a mention such as `<@123>` |
143
+ | *(none)* | guessed: whole numbers become `int`, decimals `float`, mentions are resolved, anything else is `str` |
144
+
145
+ Parameters with a default are optional, and `*args` collects any remaining arguments.
146
+
147
+ ### Command errors
148
+
149
+ If someone uses a command the wrong way, the bot tells them instead of failing silently:
150
+
151
+ ```text
152
+ !add
153
+ Missing required argument 'a'.
154
+ Usage: `!add <a> [b]`
155
+ ```
156
+
157
+ 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.
158
+
159
+ To handle these yourself, register a `command_error` handler. It replaces the default replies:
160
+
161
+ ```python
162
+ from yappy import UnknownCommand
163
+
164
+ @bot.event("command_error")
165
+ async def on_command_error(ctx, error):
166
+ if isinstance(error, UnknownCommand):
167
+ return
168
+ await ctx.reply(str(error))
169
+ ```
170
+
171
+ `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.
172
+
173
+ ### Logging
174
+
175
+ 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.
176
+
177
+ To configure logging yourself, create the app with `configure_logging=False`:
178
+
179
+ ```python
180
+ import logging
181
+ from yappy import App, setup_logging
182
+
183
+ bot = App(configure_logging=False)
184
+ setup_logging(level=logging.WARNING, show_locals=True) # or attach your own handlers to logging.getLogger("yappy")
185
+ ```
186
+
126
187
  ### Context and models
127
188
 
128
189
  YapPy currently provides these core models:
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "python-yappy"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "API Bindings for Yapper!"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
7
7
  dependencies = [
8
8
  "aiohttp>=3.14.5",
9
9
  "load-dotenv>=0.1.0",
10
+ "rich>=13.0",
10
11
  "uv-build>=0.13.1",
11
12
  ]
12
13
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-yappy"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "API Bindings for Yapper!"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -10,6 +10,7 @@ requires-python = ">=3.14"
10
10
  dependencies = [
11
11
  "aiohttp>=3.14.5",
12
12
  "load-dotenv>=0.1.0",
13
+ "rich>=13.0",
13
14
  "uv-build>=0.13.1",
14
15
  ]
15
16
 
@@ -0,0 +1,6 @@
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
@@ -1,7 +1,11 @@
1
1
  from .consts import BASE_URL
2
2
  import aiohttp
3
+ import logging
3
4
  import random
4
5
  import asyncio
6
+ import time
7
+
8
+ log = logging.getLogger(__name__)
5
9
 
6
10
  async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
7
11
  headers = {
@@ -9,10 +13,17 @@ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=
9
13
  "Content-Type": "application/json"
10
14
  }
11
15
 
16
+ started = time.perf_counter()
12
17
  try:
13
18
  async with session.request(method, BASE_URL + path, json=body, headers=headers, timeout=40) as r:
14
19
  r.raise_for_status()
15
- 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
16
27
  except aiohttp.ClientResponseError as e:
17
28
  try:
18
29
  err = await e.history[-1].json() if e.history else {}
@@ -25,8 +36,12 @@ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=
25
36
  wait = int(e.headers.get("Retry-After") or 0) if e.status == 429 or busy else 0
26
37
 
27
38
  if 0 < wait <= 60 and tries > 1:
28
- print(f"{'busy' if busy else 'rate limited'}, retrying in {wait}s")
29
- await asyncio.sleep(wait * (1 + random.random() * 0.5))
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)
30
45
  return await api_push(session, method, path, body, tries - 1)
31
46
 
32
47
  raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None