roboat 2.0.0__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.
roboat/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ roboat v2.0.0 — The best Roblox API wrapper for Python.
3
+
4
+ Quick start::
5
+
6
+ from roboat import RoboatClient, ClientBuilder
7
+ from roboat import AsyncRoboatClient
8
+ from roboat import RoboatCloudClient
9
+ from roboat import RoboatSession
10
+ from roboat.analytics import Analytics
11
+
12
+ # Sync client
13
+ client = (
14
+ ClientBuilder()
15
+ .set_cookie("ROBLOSECURITY")
16
+ .set_cache_ttl(60)
17
+ .set_rate_limit(10)
18
+ .build()
19
+ )
20
+
21
+ # Async client
22
+ async with AsyncRoboatClient(cookie="...") as c:
23
+ user = await c.users.get_user(156)
24
+
25
+ # Open Cloud (API key)
26
+ cloud = RoboatCloudClient(api_key="roblox-KEY-xxx")
27
+ cloud.datastores.set(123, "Store", "key", {"coins": 500})
28
+
29
+ # Terminal
30
+ python -m roboat
31
+ """
32
+
33
+ from .client import RoboatClient, ClientBuilder
34
+ from .async_client import AsyncRoboatClient
35
+ from .opencloud import RoboatCloudClient
36
+ from .session import RoboatSession
37
+ from .database import SessionDatabase
38
+ from .events import EventPoller
39
+ from .analytics import Analytics
40
+ from .utils import TokenBucket, TTLCache, Paginator, retry, cached
41
+
42
+ from .models import (
43
+ User, UserPresence,
44
+ Game, GameVotes, GameServer,
45
+ CatalogItem, ResaleData,
46
+ Group, GroupRole,
47
+ Friend,
48
+ Badge,
49
+ Avatar, AvatarAsset, AvatarColors,
50
+ RobuxBalance, Transaction,
51
+ Page,
52
+ )
53
+ from .trades import Trade, TradeOffer, TradeAsset
54
+ from .messages import Message, ChatConversation
55
+ from .inventory import InventoryAsset
56
+ from .develop import Universe, Place, DataStore
57
+ from .groups import GroupShout, GroupPayout, GroupJoinRequest, GroupRelationship
58
+
59
+ from .exceptions import (
60
+ RobloxAPIError,
61
+ NotAuthenticatedError,
62
+ UserNotFoundError,
63
+ GameNotFoundError,
64
+ ItemNotFoundError,
65
+ GroupNotFoundError,
66
+ BadgeNotFoundError,
67
+ RateLimitedError,
68
+ InvalidCookieError,
69
+ InsufficientFundsError,
70
+ DatabaseError,
71
+ )
72
+
73
+ __version__ = "2.0.0"
74
+ __author__ = "roboat contributors"
75
+ __license__ = "MIT"
roboat/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ Allow running as: python -m roboat
3
+ """
4
+ from roboat.session import _cli_entry
5
+
6
+ if __name__ == "__main__":
7
+ _cli_entry()
roboat/analytics.py ADDED
@@ -0,0 +1,343 @@
1
+ """
2
+ roboat.analytics
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ High-level analytics and reporting layer.
5
+ Combines multiple API calls into ready-to-use summaries and comparisons.
6
+
7
+ Example::
8
+
9
+ from roboat import RoboatClient
10
+ from roboat.analytics import Analytics
11
+
12
+ client = RoboatClient(cookie="...")
13
+ an = Analytics(client)
14
+
15
+ report = an.user_report(156)
16
+ print(report)
17
+
18
+ comp = an.compare_games([2753915549, 286090429])
19
+ print(comp)
20
+ """
21
+
22
+ from __future__ import annotations
23
+ from dataclasses import dataclass, field
24
+ from typing import List, Optional, Dict
25
+ import threading
26
+
27
+
28
+ @dataclass
29
+ class UserReport:
30
+ user_id: int
31
+ username: str
32
+ display_name: str
33
+ is_banned: bool
34
+ has_verified_badge: bool
35
+ friend_count: int
36
+ follower_count: int
37
+ following_count: int
38
+ badge_count: int
39
+ group_count: int
40
+ total_rap: int
41
+ collectible_count: int
42
+ presence_status: str
43
+ avatar_url: Optional[str]
44
+ games: List[dict] = field(default_factory=list)
45
+
46
+ def __str__(self) -> str:
47
+ banned = " [BANNED]" if self.is_banned else ""
48
+ verify = " ✓" if self.has_verified_badge else ""
49
+ lines = [
50
+ f"",
51
+ f" ╔══ User Report {'═'*38}╗",
52
+ f" ║ {self.display_name} (@{self.username}){verify}{banned}",
53
+ f" ║ ID : {self.user_id}",
54
+ f" ║ Status : {self.presence_status}",
55
+ f" ╠{'═'*54}╣",
56
+ f" ║ Friends : {self.friend_count:>10,}",
57
+ f" ║ Followers : {self.follower_count:>10,}",
58
+ f" ║ Following : {self.following_count:>10,}",
59
+ f" ║ Badges : {self.badge_count:>10,}",
60
+ f" ║ Groups : {self.group_count:>10,}",
61
+ f" ╠{'═'*54}╣",
62
+ f" ║ Total RAP : {self.total_rap:>9,}R$",
63
+ f" ║ Collectibles: {self.collectible_count:>10,}",
64
+ ]
65
+ if self.games:
66
+ lines.append(f" ╠{'═'*54}╣")
67
+ lines.append(f" ║ Created Games ({len(self.games)}):")
68
+ for g in self.games[:3]:
69
+ lines.append(f" ║ • {g.get('name','?')[:40]}")
70
+ if self.avatar_url:
71
+ lines.append(f" ╠{'═'*54}╣")
72
+ lines.append(f" ║ Avatar: {self.avatar_url}")
73
+ lines.append(f" ╚{'═'*54}╝")
74
+ return "\n".join(lines)
75
+
76
+
77
+ @dataclass
78
+ class GameComparison:
79
+ games: list
80
+ votes: list
81
+
82
+ def __str__(self) -> str:
83
+ lines = [
84
+ "",
85
+ f" ╔══ Game Comparison {'═'*35}╗",
86
+ f" ║ {'Name':<30} {'Visits':>12} {'Playing':>8} {'👍%':>6}",
87
+ f" ╠{'═'*54}╣",
88
+ ]
89
+ vote_map = {v.universe_id: v for v in self.votes}
90
+ for g in self.games:
91
+ v = vote_map.get(g.id)
92
+ ratio = f"{v.ratio}%" if v else "N/A"
93
+ name = g.name[:28]
94
+ lines.append(
95
+ f" ║ {name:<30} {g.visits:>12,} {g.playing:>8,} {ratio:>6}"
96
+ )
97
+ lines.append(f" ╚{'═'*54}╝")
98
+ return "\n".join(lines)
99
+
100
+
101
+ @dataclass
102
+ class GroupReport:
103
+ group_id: int
104
+ name: str
105
+ owner_name: Optional[str]
106
+ member_count: int
107
+ role_count: int
108
+ is_public: bool
109
+ has_verified_badge: bool
110
+ robux_balance: Optional[int]
111
+ shout: Optional[str]
112
+ ally_count: int
113
+ enemy_count: int
114
+
115
+ def __str__(self) -> str:
116
+ verified = " ✓" if self.has_verified_badge else ""
117
+ pub = "Public" if self.is_public else "Private"
118
+ balance = f"{self.robux_balance:,}R$" if self.robux_balance is not None else "N/A"
119
+ lines = [
120
+ f"",
121
+ f" ╔══ Group Report {'═'*38}╗",
122
+ f" ║ {self.name}{verified} [ID: {self.group_id}]",
123
+ f" ║ Owner : {self.owner_name}",
124
+ f" ║ Access : {pub}",
125
+ f" ╠{'═'*54}╣",
126
+ f" ║ Members : {self.member_count:>10,}",
127
+ f" ║ Roles : {self.role_count:>10,}",
128
+ f" ║ Allies : {self.ally_count:>10,}",
129
+ f" ║ Enemies : {self.enemy_count:>10,}",
130
+ f" ║ Balance : {balance:>10}",
131
+ ]
132
+ if self.shout:
133
+ shout_short = self.shout[:45] + "…" if len(self.shout) > 45 else self.shout
134
+ lines.append(f" ╠{'═'*54}╣")
135
+ lines.append(f" ║ Shout: {shout_short}")
136
+ lines.append(f" ╚{'═'*54}╝")
137
+ return "\n".join(lines)
138
+
139
+
140
+ class Analytics:
141
+ """
142
+ High-level analytics layer that aggregates multiple API calls.
143
+ All methods return rich dataclasses with formatted __str__ output.
144
+
145
+ Args:
146
+ client: A RoboatClient instance.
147
+
148
+ Example::
149
+
150
+ an = Analytics(client)
151
+ print(an.user_report(156))
152
+ print(an.compare_games([2753915549, 286090429]))
153
+ print(an.group_report(7))
154
+ """
155
+
156
+ def __init__(self, client):
157
+ self._c = client
158
+
159
+ def user_report(self, user_id: int,
160
+ include_games: bool = True,
161
+ include_rap: bool = True) -> UserReport:
162
+ """
163
+ Build a comprehensive profile report for a user.
164
+ Fetches user info, social counts, presence, avatar, groups,
165
+ badges, and optionally games + RAP in parallel.
166
+ """
167
+ results: Dict[str, any] = {}
168
+ errors: Dict[str, Exception] = {}
169
+
170
+ def fetch(key, fn):
171
+ try:
172
+ results[key] = fn()
173
+ except Exception as e:
174
+ errors[key] = e
175
+ results[key] = None
176
+
177
+ tasks = [
178
+ ("user", lambda: self._c.users.get_user(user_id)),
179
+ ("friends", lambda: self._c.friends.get_friend_count(user_id)),
180
+ ("followers", lambda: self._c.friends.get_follower_count(user_id)),
181
+ ("following", lambda: self._c.friends.get_following_count(user_id)),
182
+ ("presence", lambda: self._c.presence.get_presence(user_id)),
183
+ ("groups", lambda: self._c.groups.get_user_groups(user_id)),
184
+ ("badges", lambda: self._c.badges.get_user_badges(user_id, limit=1)),
185
+ ("avatar_url", lambda: self._c.thumbnails.get_avatar_url(user_id)),
186
+ ]
187
+ if include_games:
188
+ tasks.append(("games", lambda: self._c.games.get_user_games(user_id, limit=5)))
189
+ if include_rap:
190
+ tasks.append(("collectibles", lambda: self._c.inventory.get_collectibles(user_id, limit=100)))
191
+
192
+ threads = [threading.Thread(target=fetch, args=(k, fn)) for k, fn in tasks]
193
+ for t in threads: t.start()
194
+ for t in threads: t.join()
195
+
196
+ user = results.get("user")
197
+ if not user:
198
+ raise ValueError(f"Could not fetch user {user_id}")
199
+
200
+ collectibles = results.get("collectibles")
201
+ total_rap = sum(a.recent_average_price for a in (collectibles.data if collectibles else []))
202
+ collectible_count = len(collectibles.data) if collectibles else 0
203
+ if collectibles and collectibles.next_cursor:
204
+ # There are more pages — do a full count
205
+ try:
206
+ total_rap = self._c.inventory.get_total_rap(user_id)
207
+ except Exception:
208
+ pass
209
+
210
+ games_page = results.get("games")
211
+ games_list = []
212
+ if games_page:
213
+ games_list = [{"name": g.name, "visits": g.visits} for g in games_page.data]
214
+
215
+ badges_page = results.get("badges")
216
+ # Roblox doesn't return a total count from this endpoint — use page size as proxy
217
+ badge_count = len(badges_page.data) if badges_page else 0
218
+
219
+ return UserReport(
220
+ user_id=user.id,
221
+ username=user.name,
222
+ display_name=user.display_name,
223
+ is_banned=user.is_banned,
224
+ has_verified_badge=user.has_verified_badge,
225
+ friend_count=results.get("friends") or 0,
226
+ follower_count=results.get("followers") or 0,
227
+ following_count=results.get("following") or 0,
228
+ badge_count=badge_count,
229
+ group_count=len(results.get("groups") or []),
230
+ total_rap=total_rap,
231
+ collectible_count=collectible_count,
232
+ presence_status=(results.get("presence") or type("P", (), {"status": "Unknown"})()).status,
233
+ avatar_url=results.get("avatar_url"),
234
+ games=games_list,
235
+ )
236
+
237
+ def compare_games(self, universe_ids: List[int]) -> GameComparison:
238
+ """
239
+ Fetch and compare multiple games side by side.
240
+ Returns a GameComparison with visit counts, player counts, and vote ratios.
241
+ """
242
+ games = self._c.games.get_games(universe_ids)
243
+ votes = self._c.games.get_votes(universe_ids)
244
+ return GameComparison(games=games, votes=votes)
245
+
246
+ def group_report(self, group_id: int) -> GroupReport:
247
+ """
248
+ Build a comprehensive report for a group.
249
+ Fetches group info, roles, shout, allies, enemies, and optionally balance.
250
+ """
251
+ results: Dict[str, any] = {}
252
+
253
+ def fetch(key, fn):
254
+ try:
255
+ results[key] = fn()
256
+ except Exception:
257
+ results[key] = None
258
+
259
+ tasks = [
260
+ ("group", lambda: self._c.groups.get_group(group_id)),
261
+ ("roles", lambda: self._c.groups.get_roles(group_id)),
262
+ ("shout", lambda: self._c.groups.get_group_shout(group_id)),
263
+ ("allies", lambda: self._c.groups.get_allies(group_id)),
264
+ ("enemies", lambda: self._c.groups.get_enemies(group_id)),
265
+ ]
266
+ if self._c.is_authenticated:
267
+ tasks.append(("balance", lambda: self._c.economy.get_group_funds(group_id)))
268
+
269
+ threads = [threading.Thread(target=fetch, args=(k, fn)) for k, fn in tasks]
270
+ for t in threads: t.start()
271
+ for t in threads: t.join()
272
+
273
+ group = results.get("group")
274
+ if not group:
275
+ raise ValueError(f"Could not fetch group {group_id}")
276
+
277
+ shout = results.get("shout")
278
+ balance_obj = results.get("balance")
279
+
280
+ return GroupReport(
281
+ group_id=group.id,
282
+ name=group.name,
283
+ owner_name=group.owner_name,
284
+ member_count=group.member_count,
285
+ role_count=len(results.get("roles") or []),
286
+ is_public=group.is_public,
287
+ has_verified_badge=group.has_verified_badge,
288
+ robux_balance=balance_obj.robux if balance_obj else None,
289
+ shout=shout.body if shout else None,
290
+ ally_count=len(results.get("allies") or []),
291
+ enemy_count=len(results.get("enemies") or []),
292
+ )
293
+
294
+ def leaderboard(self, universe_ids: List[int],
295
+ by: str = "visits") -> List[dict]:
296
+ """
297
+ Rank games by a stat.
298
+
299
+ Args:
300
+ by: "visits", "playing", "favorites", "ratio" (vote %)
301
+
302
+ Returns:
303
+ Sorted list of dicts with game info + stat.
304
+ """
305
+ games = self._c.games.get_games(universe_ids)
306
+ votes = self._c.games.get_votes(universe_ids)
307
+ vmap = {v.universe_id: v for v in votes}
308
+ favs = {uid: self._c.games.get_favorite_count(uid) for uid in universe_ids}
309
+
310
+ rows = []
311
+ for g in games:
312
+ v = vmap.get(g.id)
313
+ rows.append({
314
+ "rank": 0,
315
+ "name": g.name,
316
+ "id": g.id,
317
+ "visits": g.visits,
318
+ "playing": g.playing,
319
+ "favorites": favs.get(g.id, 0),
320
+ "ratio": v.ratio if v else 0.0,
321
+ })
322
+
323
+ key_map = {"visits": "visits", "playing": "playing",
324
+ "favorites": "favorites", "ratio": "ratio"}
325
+ rows.sort(key=lambda r: r[key_map.get(by, "visits")], reverse=True)
326
+ for i, row in enumerate(rows, 1):
327
+ row["rank"] = i
328
+ return rows
329
+
330
+ def rich_leaderboard_str(self, universe_ids: List[int],
331
+ by: str = "visits") -> str:
332
+ """Return a formatted string leaderboard table."""
333
+ rows = self.leaderboard(universe_ids, by=by)
334
+ header = f" {'#':>3} {'Name':<32} {'Visits':>12} {'Playing':>8} {'Ratio':>7}"
335
+ divider = " " + "─" * 68
336
+ lines = [f"\n 🏆 Game Leaderboard (by {by})", divider, header, divider]
337
+ for r in rows:
338
+ lines.append(
339
+ f" {r['rank']:>3}. {r['name'][:30]:<32} "
340
+ f"{r['visits']:>12,} {r['playing']:>8,} {r['ratio']:>6.1f}%"
341
+ )
342
+ lines.append(divider)
343
+ return "\n".join(lines)