robase-utils 2.3.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.
@@ -0,0 +1 @@
1
+ roboat_utils
@@ -0,0 +1,128 @@
1
+ """
2
+ roboat-utils v3.0.0
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+ Python interface for the Roblox ecosystem.
5
+ Install: pip install roboat-utils
6
+
7
+ Quick start::
8
+
9
+ from roboat_utils import RoboatClient
10
+
11
+ client = RoboatClient(cookie="_|WARNING:...")
12
+ user = client.users.get_user(156)
13
+ print(user)
14
+
15
+ Async::
16
+
17
+ from roboat_utils import AsyncRoboatClient
18
+ import asyncio
19
+
20
+ async def main():
21
+ async with AsyncRoboatClient(cookie="...") as client:
22
+ user = await client.users.get_user(156)
23
+
24
+ asyncio.run(main())
25
+
26
+ Open Cloud::
27
+
28
+ from roboat_utils import RoboatCloudClient
29
+
30
+ cloud = RoboatCloudClient(api_key="roblox-KEY-xxxx")
31
+ cloud.datastores.set(123456, "PlayerData", "player_1", {"coins": 500})
32
+ """
33
+
34
+ from .client import RoboatClient, ClientBuilder
35
+ from .async_client import AsyncRoboatClient
36
+ from .opencloud import RoboatCloudClient
37
+ from .session import RoboatSession
38
+ from .database import SessionDatabase
39
+ from .events import EventPoller
40
+ from .analytics import Analytics
41
+ from .oauth import OAuthManager, get_oauth_url, OAUTH_URL
42
+ from .utils import TokenBucket, TTLCache, Paginator, retry, cached
43
+
44
+ from .models import (
45
+ User, UserPresence,
46
+ Game, GameVotes, GameServer,
47
+ CatalogItem, ResaleData,
48
+ Group, GroupRole,
49
+ Friend,
50
+ Badge,
51
+ Avatar, AvatarAsset, AvatarColors,
52
+ RobuxBalance, Transaction,
53
+ Page,
54
+ )
55
+ from .trades import Trade, TradeOffer, TradeAsset
56
+ from .messages import Message, ChatConversation
57
+ from .inventory import InventoryAsset
58
+ from .develop import Universe, Place, DataStore, PlaceVersion, TeamCreateMember, PluginInfo
59
+ from .groups import GroupShout, GroupPayout, GroupJoinRequest, GroupRelationship
60
+ from .marketplace import MarketplaceAPI, LimitedData, ResaleProfit, RAPTracker
61
+ from .social import SocialGraph, UserNode, PresenceSnapshot
62
+ from .notifications import NotificationsAPI, NotificationResult
63
+ from .publish import PublishAPI, UploadedAsset
64
+ from .moderation import ModerationAPI, AccountStanding, AbuseReport
65
+
66
+ from .exceptions import (
67
+ RoboatAPIError,
68
+ HTTPError,
69
+ RateLimitedError,
70
+ InvalidCookieError,
71
+ ForbiddenError,
72
+ NotFoundError,
73
+ UserNotFoundError,
74
+ GameNotFoundError,
75
+ ItemNotFoundError,
76
+ GroupNotFoundError,
77
+ BadgeNotFoundError,
78
+ ServerError,
79
+ NotAuthenticatedError,
80
+ InsufficientFundsError,
81
+ DatabaseError,
82
+ )
83
+
84
+ __version__ = "3.0.0"
85
+ __author__ = "roboat contributors"
86
+ __license__ = "MIT"
87
+
88
+ __all__ = [
89
+ # Clients
90
+ "RoboatClient", "ClientBuilder",
91
+ "AsyncRoboatClient",
92
+ "RoboatCloudClient",
93
+ "RoboatSession",
94
+ "SessionDatabase",
95
+ # High-level
96
+ "EventPoller", "Analytics", "OAuthManager",
97
+ "get_oauth_url", "OAUTH_URL",
98
+ # Utils
99
+ "TokenBucket", "TTLCache", "Paginator", "retry", "cached",
100
+ # Models
101
+ "User", "UserPresence",
102
+ "Game", "GameVotes", "GameServer",
103
+ "CatalogItem", "ResaleData",
104
+ "Group", "GroupRole",
105
+ "Friend", "Badge",
106
+ "Avatar", "AvatarAsset", "AvatarColors",
107
+ "RobuxBalance", "Transaction",
108
+ "Page",
109
+ "Trade", "TradeOffer", "TradeAsset",
110
+ "Message", "ChatConversation",
111
+ "InventoryAsset",
112
+ "Universe", "Place", "DataStore", "PlaceVersion",
113
+ "TeamCreateMember", "PluginInfo",
114
+ "GroupShout", "GroupPayout", "GroupJoinRequest", "GroupRelationship",
115
+ "LimitedData", "ResaleProfit", "RAPTracker",
116
+ "UserNode", "PresenceSnapshot",
117
+ "NotificationResult", "UploadedAsset",
118
+ "AccountStanding", "AbuseReport",
119
+ # APIs
120
+ "MarketplaceAPI", "SocialGraph", "NotificationsAPI", "PublishAPI", "ModerationAPI",
121
+ # Exceptions
122
+ "RoboatAPIError", "HTTPError",
123
+ "RateLimitedError", "InvalidCookieError", "ForbiddenError",
124
+ "NotFoundError", "UserNotFoundError", "GameNotFoundError",
125
+ "ItemNotFoundError", "GroupNotFoundError", "BadgeNotFoundError",
126
+ "ServerError", "NotAuthenticatedError", "InsufficientFundsError",
127
+ "DatabaseError",
128
+ ]
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m roboat_utils"""
2
+ from roboat_utils.session import _cli_entry
3
+
4
+ if __name__ == "__main__":
5
+ _cli_entry()
@@ -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_utils.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)