python-yappy 0.1.0__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.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.3
2
+ Name: python-yappy
3
+ Version: 0.1.0
4
+ Summary: API Bindings for Yapper!
5
+ Author: H1387Lmao
6
+ Requires-Dist: aiohttp>=3.14.5
7
+ Requires-Dist: load-dotenv>=0.1.0
8
+ Requires-Dist: uv-build>=0.13.1
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ # YapPy
13
+
14
+ **A Python API binding for Yapper.**
15
+
16
+ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a chat platform similar to Discord. It provides familiar models, event handlers, and command decorators so you can build Yapper bots with straightforward asynchronous Python.
17
+
18
+ > **Project status:** Early development. The current implementation includes `Context`, `Message`, `Channel`, and `User` models, along with bot event and command handling. The API may change as development continues.
19
+
20
+ ## Features
21
+
22
+ - **Python-first API** — build bot logic using async functions and decorators.
23
+ - **Event handlers** — respond to Yapper events such as incoming messages.
24
+ - **Commands** — register named commands with descriptions.
25
+ - **Context-based interactions** — reply to messages through a context object.
26
+ - **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
27
+ - **Optional logging** — enable verbose output when creating the application.
28
+
29
+ ## Installation
30
+
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
+
47
+ ```bash
48
+ pip install python-dotenv
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ Create a file named `bot.py`:
54
+
55
+ ```python
56
+ import os
57
+
58
+ from dotenv import load_dotenv
59
+ from yappy import App
60
+
61
+ load_dotenv()
62
+
63
+ bot = App(verbose=True)
64
+
65
+
66
+ @bot.event("message")
67
+ async def on_message(ctx):
68
+ if ctx.is_bot:
69
+ return
70
+
71
+ await ctx.reply("Hi!")
72
+
73
+
74
+ @bot.command(
75
+ name="ping",
76
+ description="replies with 'Pong!'",
77
+ )
78
+ async def ping(ctx):
79
+ await ctx.reply("Pong!")
80
+
81
+
82
+ bot.run(os.getenv("TOKEN"))
83
+ ```
84
+
85
+ Create a `.env` file beside `bot.py`:
86
+
87
+ ```dotenv
88
+ TOKEN=your_yapper_bot_token
89
+ ```
90
+
91
+ Then run the bot:
92
+
93
+ ```bash
94
+ python bot.py
95
+ ```
96
+
97
+ This example registers a `message` event that replies with `Hi!` to messages not sent by bots, and a `ping` command that replies with `Pong!`.
98
+
99
+ **Keep your token private.** Do not commit your `.env` file or publish your bot token.
100
+
101
+ ## Core concepts
102
+
103
+ ### `App`
104
+
105
+ `App` is the main entry point for creating and running a YapPy bot.
106
+
107
+ ```python
108
+ from yappy import App
109
+
110
+ bot = App(verbose=True)
111
+ bot.run("YOUR_TOKEN")
112
+ ```
113
+
114
+ Set `verbose=True` to enable verbose output while developing. Keep credentials in environment variables rather than hard-coding them in source files.
115
+
116
+ ### Events
117
+
118
+ Register an event handler with `@bot.event(...)`. Event handlers are asynchronous functions.
119
+
120
+ ```python
121
+ @bot.event("message")
122
+ async def on_message(ctx):
123
+ if ctx.is_bot:
124
+ return
125
+
126
+ await ctx.reply("Hello!")
127
+ ```
128
+
129
+ The event name and handler arguments should match the event interface supported by the installed YapPy version.
130
+
131
+ ### Commands
132
+
133
+ Register a command using `@bot.command(...)`:
134
+
135
+ ```python
136
+ @bot.command(
137
+ name="ping",
138
+ description="replies with 'Pong!'",
139
+ )
140
+ async def ping(ctx):
141
+ await ctx.reply("Pong!")
142
+ ```
143
+
144
+ The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
145
+
146
+ ### Context and models
147
+
148
+ YapPy currently provides these core models:
149
+
150
+ | Model | Purpose |
151
+ | --- | --- |
152
+ | `Context` | Context for handling an interaction or message event. |
153
+ | `Message` | Represents a message. |
154
+ | `Channel` | Represents a channel. |
155
+ | `User` | Represents a user. |
156
+
157
+ 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
+
159
+ ## Project status
160
+
161
+ YapPy is under active development. Current implemented areas include:
162
+
163
+ - `Context`, `Message`, `Channel`, and `User` models
164
+ - Bot event registration and handling
165
+ - Command registration
166
+ - Message replies through the context API
167
+
168
+ Other features may be planned, incomplete, or unsupported. Check the source code and release notes for the actual capabilities of the version you are using.
169
+
170
+ ## Contributing
171
+
172
+ Contributions, bug reports, and suggestions are welcome. Before opening a pull request, describe the change clearly and include a minimal reproduction or test where appropriate.
173
+
174
+ ## License
175
+
176
+ This project is licensed under the MIT License.
@@ -0,0 +1,165 @@
1
+ # YapPy
2
+
3
+ **A Python API binding for Yapper.**
4
+
5
+ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a chat platform similar to Discord. It provides familiar models, event handlers, and command decorators so you can build Yapper bots with straightforward asynchronous Python.
6
+
7
+ > **Project status:** Early development. The current implementation includes `Context`, `Message`, `Channel`, and `User` models, along with bot event and command handling. The API may change as development continues.
8
+
9
+ ## Features
10
+
11
+ - **Python-first API** — build bot logic using async functions and decorators.
12
+ - **Event handlers** — respond to Yapper events such as incoming messages.
13
+ - **Commands** — register named commands with descriptions.
14
+ - **Context-based interactions** — reply to messages through a context object.
15
+ - **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
16
+ - **Optional logging** — enable verbose output when creating the application.
17
+
18
+ ## Installation
19
+
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
+
36
+ ```bash
37
+ pip install python-dotenv
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ Create a file named `bot.py`:
43
+
44
+ ```python
45
+ import os
46
+
47
+ from dotenv import load_dotenv
48
+ from yappy import App
49
+
50
+ load_dotenv()
51
+
52
+ bot = App(verbose=True)
53
+
54
+
55
+ @bot.event("message")
56
+ async def on_message(ctx):
57
+ if ctx.is_bot:
58
+ return
59
+
60
+ await ctx.reply("Hi!")
61
+
62
+
63
+ @bot.command(
64
+ name="ping",
65
+ description="replies with 'Pong!'",
66
+ )
67
+ async def ping(ctx):
68
+ await ctx.reply("Pong!")
69
+
70
+
71
+ bot.run(os.getenv("TOKEN"))
72
+ ```
73
+
74
+ Create a `.env` file beside `bot.py`:
75
+
76
+ ```dotenv
77
+ TOKEN=your_yapper_bot_token
78
+ ```
79
+
80
+ Then run the bot:
81
+
82
+ ```bash
83
+ python bot.py
84
+ ```
85
+
86
+ This example registers a `message` event that replies with `Hi!` to messages not sent by bots, and a `ping` command that replies with `Pong!`.
87
+
88
+ **Keep your token private.** Do not commit your `.env` file or publish your bot token.
89
+
90
+ ## Core concepts
91
+
92
+ ### `App`
93
+
94
+ `App` is the main entry point for creating and running a YapPy bot.
95
+
96
+ ```python
97
+ from yappy import App
98
+
99
+ bot = App(verbose=True)
100
+ bot.run("YOUR_TOKEN")
101
+ ```
102
+
103
+ Set `verbose=True` to enable verbose output while developing. Keep credentials in environment variables rather than hard-coding them in source files.
104
+
105
+ ### Events
106
+
107
+ Register an event handler with `@bot.event(...)`. Event handlers are asynchronous functions.
108
+
109
+ ```python
110
+ @bot.event("message")
111
+ async def on_message(ctx):
112
+ if ctx.is_bot:
113
+ return
114
+
115
+ await ctx.reply("Hello!")
116
+ ```
117
+
118
+ The event name and handler arguments should match the event interface supported by the installed YapPy version.
119
+
120
+ ### Commands
121
+
122
+ Register a command using `@bot.command(...)`:
123
+
124
+ ```python
125
+ @bot.command(
126
+ name="ping",
127
+ description="replies with 'Pong!'",
128
+ )
129
+ async def ping(ctx):
130
+ await ctx.reply("Pong!")
131
+ ```
132
+
133
+ The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
134
+
135
+ ### Context and models
136
+
137
+ YapPy currently provides these core models:
138
+
139
+ | Model | Purpose |
140
+ | --- | --- |
141
+ | `Context` | Context for handling an interaction or message event. |
142
+ | `Message` | Represents a message. |
143
+ | `Channel` | Represents a channel. |
144
+ | `User` | Represents a user. |
145
+
146
+ 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
+
148
+ ## Project status
149
+
150
+ YapPy is under active development. Current implemented areas include:
151
+
152
+ - `Context`, `Message`, `Channel`, and `User` models
153
+ - Bot event registration and handling
154
+ - Command registration
155
+ - Message replies through the context API
156
+
157
+ Other features may be planned, incomplete, or unsupported. Check the source code and release notes for the actual capabilities of the version you are using.
158
+
159
+ ## Contributing
160
+
161
+ Contributions, bug reports, and suggestions are welcome. Before opening a pull request, describe the change clearly and include a minimal reproduction or test where appropriate.
162
+
163
+ ## License
164
+
165
+ This project is licensed under the MIT License.
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "python-yappy"
3
+ version = "0.1.0"
4
+ description = "API Bindings for Yapper!"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "aiohttp>=3.14.5",
9
+ "load-dotenv>=0.1.0",
10
+ "uv-build>=0.13.1",
11
+ ]
12
+
13
+ [[project.authors]]
14
+ name = "H1387Lmao"
15
+
16
+ [tool.uv.build-backend]
17
+ module-name = "yappy"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.12.1,<0.13.1"]
21
+ build-backend = "uv_build"
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "python-yappy"
3
+ version = "0.1.0"
4
+ description = "API Bindings for Yapper!"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "H1387Lmao"}
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "aiohttp>=3.14.5",
12
+ "load-dotenv>=0.1.0",
13
+ "uv-build>=0.13.1",
14
+ ]
15
+
16
+ [tool.uv.build-backend]
17
+ module-name = "yappy"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.12.1,<0.13.1"]
21
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ from .app import App
@@ -0,0 +1,44 @@
1
+ from .consts import BASE_URL
2
+ import aiohttp
3
+
4
+ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
5
+ headers = {
6
+ "Authorization": f"Bot {session._bearer_token}",
7
+ "Content-Type": "application/json"
8
+ }
9
+
10
+ try:
11
+ async with session.request(method, BASE_URL + path, json=body, headers=headers, timeout=40) as r:
12
+ r.raise_for_status()
13
+ return await r.json()
14
+ except aiohttp.ClientResponseError as e:
15
+ try:
16
+ err = await e.history[-1].json() if e.history else {}
17
+ except Exception:
18
+ err = {}
19
+
20
+ msg = err.get("error") if isinstance(err, dict) else None
21
+ busy = e.status == 503 and isinstance(err, dict) and "retry_after" in err
22
+
23
+ wait = int(e.headers.get("Retry-After") or 0) if e.status == 429 or busy else 0
24
+
25
+ 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)
29
+
30
+ raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None
31
+
32
+ class api:
33
+ @staticmethod
34
+ async def put(session, path="", body=None, tries=3):
35
+ await api_push(session, "PUT", path, body, tries)
36
+ @staticmethod
37
+ async def get(session, path="", body=None, tries=3):
38
+ return await api_push(session, "GET", path, body, tries)
39
+ @staticmethod
40
+ async def post(session, path="", body=None, tries=3):
41
+ await api_push(session, "POST", path, body, tries)
42
+ @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)
@@ -0,0 +1,204 @@
1
+ from .core import *
2
+ from .consts import BASE_URL
3
+ from .api import *
4
+ from .errors import ImproperToken, InvalidSignature
5
+
6
+ import inspect
7
+ import asyncio
8
+ import aiohttp
9
+ from functools import wraps
10
+ from typing import Callable
11
+
12
+ class App:
13
+ def __init__(self, verbose=False):
14
+ self.__commands = {}
15
+ self.__event_handlers = {}
16
+
17
+ self.user = None
18
+ self._token = None
19
+
20
+ self._verbose = verbose
21
+
22
+ self.__cache = {"users":{}}
23
+
24
+ async def _load_bot_info(self):
25
+ my_info = await api.get(self.session, "/me")
26
+
27
+ if self._verbose:
28
+ print(my_info)
29
+
30
+ return Bot(
31
+ name=my_info["username"],
32
+ display_name=my_info["display_name"],
33
+ id=my_info["id"],
34
+ profile=Profile(
35
+ avatar=my_info["avatar_url"],
36
+ banner=my_info["banner_url"],
37
+ ),
38
+ bio=my_info["bio"],
39
+ created_at=my_info["created_at"],
40
+
41
+ owner_id=my_info["bot"]["owner_id"],
42
+ is_bot=True
43
+ )
44
+
45
+ def get_user(
46
+ self,
47
+ id,
48
+ _data
49
+ ):
50
+ if id in self.__cache["users"]:
51
+ return self.__cache["users"]
52
+ user = User(
53
+ name=_data["author"],
54
+ id=id
55
+ )
56
+
57
+ self.__cache[id]=user
58
+ return user
59
+
60
+ def _build_context(self, ev):
61
+ d = ev["data"]
62
+
63
+ channel=Channel(
64
+ self,
65
+ id=d["channel_id"],
66
+ name=d["channel_name"]
67
+ )
68
+
69
+ return Context(
70
+ self,
71
+ is_bot=ev.get("author_bot", False),
72
+ message=Message(
73
+ self,
74
+ id=d["id"],
75
+ author=self.get_user(
76
+ d["author_id"], d
77
+ ),
78
+ channel=channel,
79
+ created_at=d["created_at"],
80
+ content=d["content"]
81
+ ),
82
+ channel=channel
83
+ )
84
+
85
+ async def _handle_event(self, session: aiohttp.ClientSession, ev):
86
+ if self._verbose:
87
+ print(ev)
88
+ d = ev["data"]
89
+
90
+ if ev["type"] in self.__event_handlers:
91
+ if (cmd_name:=d.get("command")):
92
+ if cmd_name in self.__commands:
93
+ await self.__commands[cmd_name].func(
94
+ self._build_context(
95
+ ev
96
+ )
97
+ )
98
+ elif cmd_name in self.__aliases:
99
+ await self.__aliases[cmd_name](
100
+ self._build_context(
101
+ ev
102
+ )
103
+ )
104
+ else:
105
+ raise UnknownCommand(
106
+ f"Unhandled command '{cmd_name}'!"
107
+ )
108
+ else:
109
+ await self.__event_handlers[ev["type"]](
110
+ self._build_context(
111
+ ev
112
+ )
113
+ )
114
+
115
+ async def _init_commands(self):
116
+ _commands = []
117
+ for name, func in self.__commands.items():
118
+ _commands.append(
119
+ {"name": name, "description": func.description}
120
+ )
121
+
122
+ await api.put(
123
+ self.session,
124
+ "/commands",
125
+ {"commands": _commands}
126
+ )
127
+
128
+ async def _event_loop(self):
129
+ async with aiohttp.ClientSession() as self.session:
130
+ self.session._bearer_token=self._token
131
+
132
+ self.user = await self._load_bot_info()
133
+ await self._init_commands()
134
+
135
+ events_init = await api.get(self.session, "/events?after=0&wait=0")
136
+ last = events_init["last"]
137
+ fails = 0
138
+
139
+ while True:
140
+ try:
141
+ out = await api.get(self.session, f"/events?after={last}&wait=25")
142
+ fails = 0
143
+ except Exception as e:
144
+ fails += 1
145
+ if "(401)" in str(e):
146
+ raise ImproperToken("Please check if your token is correctly used!")
147
+ wait = min(30, 2 ** fails)
148
+
149
+ print(f"retrying in {wait}s:", e)
150
+
151
+ await asyncio.sleep(wait)
152
+ continue
153
+ for ev in out["events"]:
154
+ if ev["id"] <= last:
155
+ continue
156
+ last = ev["id"]
157
+ try:
158
+ await self._handle_event(self.session, ev)
159
+ except Exception as e:
160
+ print("error handling", ev["type"], "event:", e)
161
+
162
+
163
+ def event(self, event_type: str):
164
+ def wrapper(func):
165
+ if not inspect.iscoroutinefunction(func):
166
+ raise InvalidSignature(f"Expected event handler for '{event_type}' to be a coroutine!")
167
+ self.__event_handlers[
168
+ event_type
169
+ ] = func
170
+ return wrapper
171
+
172
+ def command(
173
+ self,
174
+ name, *,
175
+ aliases=[],
176
+ description=""
177
+ ):
178
+ def wrapper(
179
+ func
180
+ ):
181
+ if not inspect.iscoroutinefunction(func):
182
+ raise InvalidSignature(f"Expected command handler for '{name}' to be a coroutine!")
183
+ self.__commands[name]=Command(
184
+ name=name,
185
+ aliases=aliases,
186
+ description=description,
187
+ func=func
188
+ )
189
+
190
+ for alias in aliases:
191
+ self.__aliases[alias] = self.__commands[name]
192
+
193
+ return wrapper
194
+
195
+ def run(self, token: str):
196
+ self._token = token
197
+
198
+ self._yappy_loop = asyncio.new_event_loop()
199
+ asyncio.set_event_loop(self._yappy_loop)
200
+
201
+ try:
202
+ self._yappy_loop.run_until_complete(self._event_loop())
203
+ finally:
204
+ self._yappy_loop.close()
@@ -0,0 +1 @@
1
+ BASE_URL = "https://www.yapper.website/api/bot/v1"
@@ -0,0 +1,105 @@
1
+ from .api import *
2
+ from typing import Callable
3
+
4
+ class Profile:
5
+ def __init__(self,
6
+ avatar: str = "",
7
+ banner: str = ""
8
+ ):
9
+ self.avatar = avatar
10
+ self.banner = banner
11
+
12
+ class User:
13
+ def __init__(self,
14
+ name: str ="",
15
+ display_name: str = "",
16
+ id=0,
17
+ profile: Profile | None = None,
18
+ created_at: str = "",
19
+ bio: str = "",
20
+ is_bot: bool = True
21
+ ):
22
+ self.name = name
23
+ self.id = id
24
+ self.profile = profile
25
+ self.display_name = display_name
26
+ self.is_bot = is_bot
27
+ self.bio = bio
28
+
29
+ class Bot(User):
30
+ def __init__(self, owner_id=None, **kwargs):
31
+ super().__init__(**kwargs)
32
+ self.owner_id = owner_id
33
+
34
+ class Command:
35
+ def __init__(self,
36
+ name: str,
37
+ aliases: list[str] = [],
38
+ description: str = "",
39
+ func: Callable = None,
40
+ ):
41
+ self.name = name
42
+ self.aliases = aliases
43
+ self.description = description
44
+ self.func = func
45
+
46
+ class Channel:
47
+ def __init__(self,
48
+ bot,
49
+ id=0,
50
+ name: str = ""
51
+ ):
52
+ self.id = id
53
+ self.bot = bot
54
+ self.name = name
55
+
56
+ class Message:
57
+ def __init__(self,
58
+ bot,
59
+ id=0,
60
+ channel: Channel | None = None,
61
+ author: User | None = None,
62
+ created_at: str = "",
63
+ content: str = ""
64
+ ):
65
+ self.id = id
66
+ self.channel = channel
67
+ self.bot = bot
68
+ self.created_at = created_at
69
+ self.content = content
70
+
71
+ self.author = author
72
+
73
+ class Context:
74
+ def __init__(self,
75
+ bot,
76
+ is_bot: bool = False,
77
+ message: Message | None = None,
78
+ channel: Channel | None = None
79
+ ):
80
+ self.bot = bot
81
+ self.is_bot = is_bot
82
+ self.message = message
83
+ self.channel = channel
84
+
85
+ async def reply(
86
+ self,
87
+ content: str = "Hi"
88
+ ):
89
+ await api.send(
90
+ self.bot.session, self.channel.id, data={
91
+ "content": content,
92
+ "reply_to": self.message.id
93
+ }
94
+ )
95
+
96
+ async def send(
97
+ self,
98
+ content: str = "Hi"
99
+ ):
100
+ await api.send(
101
+ self.bot.session, self.channel.id, data={
102
+ "content": content,
103
+ "reply_to": None
104
+ }
105
+ )
@@ -0,0 +1,8 @@
1
+ class ImproperToken(Exception):
2
+ pass
3
+
4
+ class InvalidSignature(Exception):
5
+ pass
6
+
7
+ class UnknownCommand(Exception):
8
+ pass