python-yappy 0.1.0__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.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
 
@@ -13,60 +13,52 @@ 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
 
20
- Install YapPy using your project's preferred method.
21
-
22
- If the package is available on PyPI:
23
-
24
- ```bash
25
- pip install yappy
26
- ```
27
-
28
- If you are working from a local checkout, install the project from its repository directory:
29
-
30
- ```bash
31
- pip install .
32
- ```
33
-
34
- The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
35
21
 
36
22
  ```bash
37
- pip install python-dotenv
23
+ pip install python-yappy
38
24
  ```
39
25
 
40
26
  ## Quick start
41
27
 
42
- Create a file named `bot.py`:
28
+ This example uses the dotenv module from `pip install load_dotenv`
29
+ Create a file named `main.py`:
43
30
 
44
31
  ```python
32
+ from yappy import App
45
33
  import os
46
-
47
34
  from dotenv import load_dotenv
48
- from yappy import App
49
-
50
35
  load_dotenv()
51
-
52
36
  bot = App(verbose=True)
53
-
54
-
55
- @bot.event("message")
37
+ @bot.event(
38
+ "message"
39
+ )
56
40
  async def on_message(ctx):
57
- if ctx.is_bot:
58
- return
59
-
60
- await ctx.reply("Hi!")
61
-
62
-
41
+ if ctx.is_bot:
42
+ return
43
+ await bot.process_command(ctx)
63
44
  @bot.command(
64
- name="ping",
65
- description="replies with 'Pong!'",
45
+ name="ping",
46
+ description="replies with 'Pong!'"
66
47
  )
67
48
  async def ping(ctx):
68
- await ctx.reply("Pong!")
49
+ await ctx.reply("Pong!")
69
50
 
51
+ @bot.command(
52
+ name="react",
53
+ description="React your message with a certain emoji!"
54
+ )
55
+ async def react(ctx, emoji: str):
56
+ await ctx.message.react(emoji)
57
+ @bot.event(
58
+ "ready"
59
+ )
60
+ async def on_ready():
61
+ print("Logged as", bot.user.name)
70
62
 
71
63
  bot.run(os.getenv("TOKEN"))
72
64
  ```
@@ -112,7 +104,7 @@ async def on_message(ctx):
112
104
  if ctx.is_bot:
113
105
  return
114
106
 
115
- await ctx.reply("Hello!")
107
+ await bot.process_command(ctx)
116
108
  ```
117
109
 
118
110
  The event name and handler arguments should match the event interface supported by the installed YapPy version.
@@ -132,6 +124,66 @@ async def ping(ctx):
132
124
 
133
125
  The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
134
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
+
135
187
  ### Context and models
136
188
 
137
189
  YapPy currently provides these core models:
@@ -142,6 +194,8 @@ YapPy currently provides these core models:
142
194
  | `Message` | Represents a message. |
143
195
  | `Channel` | Represents a channel. |
144
196
  | `User` | Represents a user. |
197
+ | `Guild` | Represents a guild / server |
198
+ | `Role` | Represents a role |
145
199
 
146
200
  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.
147
201
 
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "python-yappy"
3
- version = "0.1.0"
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.0"
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
@@ -0,0 +1,77 @@
1
+ from .consts import BASE_URL
2
+ import aiohttp
3
+ import logging
4
+ import random
5
+ import asyncio
6
+ import time
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
11
+ headers = {
12
+ "Authorization": f"Bot {session._bearer_token}",
13
+ "Content-Type": "application/json"
14
+ }
15
+
16
+ started = time.perf_counter()
17
+ try:
18
+ async with session.request(method, BASE_URL + path, json=body, headers=headers, timeout=40) as r:
19
+ r.raise_for_status()
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
27
+ except aiohttp.ClientResponseError as e:
28
+ try:
29
+ err = await e.history[-1].json() if e.history else {}
30
+ except Exception:
31
+ err = {}
32
+
33
+ msg = err.get("error") if isinstance(err, dict) else None
34
+ busy = e.status == 503 and isinstance(err, dict) and "retry_after" in err
35
+
36
+ wait = int(e.headers.get("Retry-After") or 0) if e.status == 429 or busy else 0
37
+
38
+ if 0 < wait <= 60 and 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)
46
+
47
+ raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None
48
+
49
+ class API:
50
+ @staticmethod
51
+ async def _request(session, method, path="", body=None, tries=3):
52
+ return await api_push(session, method, path, body, tries)
53
+
54
+ @staticmethod
55
+ async def get(session, path="", body=None, tries=3):
56
+ return await API._request(session, "GET", path, body, tries)
57
+
58
+ @staticmethod
59
+ async def post(session, path="", body=None, tries=3):
60
+ return await API._request(session, "POST", path, body, tries)
61
+
62
+ @staticmethod
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
+