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.
roboat_utils/models.py ADDED
@@ -0,0 +1,520 @@
1
+ """
2
+ roboat_utils.models
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+ Typed dataclasses for every Roblox API response object.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Optional
10
+
11
+
12
+ # ── Users ──────────────────────────────────────────────────────────── #
13
+
14
+ @dataclass
15
+ class User:
16
+ id: int
17
+ name: str
18
+ display_name: str
19
+ description: str = ""
20
+ created: Optional[str] = None
21
+ is_banned: bool = False
22
+ has_verified_badge: bool = False
23
+
24
+ @classmethod
25
+ def from_dict(cls, d: dict) -> "User":
26
+ return cls(
27
+ id=d.get("id", 0),
28
+ name=d.get("name", ""),
29
+ display_name=d.get("displayName", ""),
30
+ description=d.get("description", ""),
31
+ created=d.get("created"),
32
+ is_banned=d.get("isBanned", False),
33
+ has_verified_badge=d.get("hasVerifiedBadge", False),
34
+ )
35
+
36
+ def __str__(self) -> str:
37
+ verified = " ✓" if self.has_verified_badge else ""
38
+ banned = " [BANNED]" if self.is_banned else ""
39
+ return f"{self.display_name} (@{self.name}){verified}{banned} [ID: {self.id}]"
40
+
41
+
42
+ @dataclass
43
+ class UserPresence:
44
+ user_id: int
45
+ last_online: Optional[str] = None
46
+ last_location: Optional[str] = None
47
+ game_id: Optional[str] = None
48
+ universe_id: Optional[int] = None
49
+ user_presence_type: int = 0 # 0=Offline 1=Online 2=InGame 3=InStudio
50
+
51
+ _LABELS = {0: "Offline", 1: "Online", 2: "In Game", 3: "In Studio"}
52
+
53
+ @classmethod
54
+ def from_dict(cls, d: dict) -> "UserPresence":
55
+ return cls(
56
+ user_id=d.get("userId", 0),
57
+ last_online=d.get("lastOnline"),
58
+ last_location=d.get("lastLocation"),
59
+ game_id=d.get("gameId"),
60
+ universe_id=d.get("universeId"),
61
+ user_presence_type=d.get("userPresenceType", 0),
62
+ )
63
+
64
+ @property
65
+ def status(self) -> str:
66
+ return self._LABELS.get(self.user_presence_type, "Unknown")
67
+
68
+ @property
69
+ def is_online(self) -> bool:
70
+ return self.user_presence_type > 0
71
+
72
+
73
+ # ── Games ──────────────────────────────────────────────────────────── #
74
+
75
+ @dataclass
76
+ class Game:
77
+ id: int
78
+ root_place_id: int
79
+ name: str
80
+ description: str
81
+ creator_name: str
82
+ creator_id: int
83
+ creator_type: str
84
+ price: Optional[int]
85
+ playing: int
86
+ visits: int
87
+ max_players: int
88
+ created: Optional[str]
89
+ updated: Optional[str]
90
+ genre: str
91
+ is_favorited: bool
92
+ favorited_count: int
93
+
94
+ @classmethod
95
+ def from_dict(cls, d: dict) -> "Game":
96
+ creator = d.get("creator", {})
97
+ return cls(
98
+ id=d.get("id", 0),
99
+ root_place_id=d.get("rootPlaceId", 0),
100
+ name=d.get("name", ""),
101
+ description=d.get("description", ""),
102
+ creator_name=creator.get("name", ""),
103
+ creator_id=creator.get("id", 0),
104
+ creator_type=creator.get("type", "User"),
105
+ price=d.get("price"),
106
+ playing=d.get("playing", 0),
107
+ visits=d.get("visits", 0),
108
+ max_players=d.get("maxPlayers", 0),
109
+ created=d.get("created"),
110
+ updated=d.get("updated"),
111
+ genre=d.get("genre", ""),
112
+ is_favorited=d.get("isFavoritedByUser", False),
113
+ favorited_count=d.get("favoritedCount", 0),
114
+ )
115
+
116
+ def __str__(self) -> str:
117
+ price_str = f"💰 {self.price}R$" if self.price else "Free"
118
+ return (
119
+ f"🎮 {self.name} [Universe: {self.id}]\n"
120
+ f" Creator : {self.creator_name} ({self.creator_type})\n"
121
+ f" Visits : {self.visits:,}\n"
122
+ f" Playing : {self.playing:,}\n"
123
+ f" Max : {self.max_players}\n"
124
+ f" Genre : {self.genre}\n"
125
+ f" Price : {price_str}\n"
126
+ f" Favs : {self.favorited_count:,}"
127
+ )
128
+
129
+
130
+ @dataclass
131
+ class GameVotes:
132
+ universe_id: int
133
+ up_votes: int
134
+ down_votes: int
135
+
136
+ @classmethod
137
+ def from_dict(cls, d: dict) -> "GameVotes":
138
+ return cls(
139
+ universe_id=d.get("id", 0),
140
+ up_votes=d.get("upVotes", 0),
141
+ down_votes=d.get("downVotes", 0),
142
+ )
143
+
144
+ @property
145
+ def ratio(self) -> float:
146
+ total = self.up_votes + self.down_votes
147
+ return round(self.up_votes / total * 100, 1) if total else 0.0
148
+
149
+ def __str__(self) -> str:
150
+ return f"👍 {self.up_votes:,} 👎 {self.down_votes:,} ({self.ratio}% positive)"
151
+
152
+
153
+ @dataclass
154
+ class GameServer:
155
+ id: str
156
+ max_players: int
157
+ playing: int
158
+ fps: float
159
+ ping: int
160
+
161
+ @classmethod
162
+ def from_dict(cls, d: dict) -> "GameServer":
163
+ return cls(
164
+ id=d.get("id", ""),
165
+ max_players=d.get("maxPlayers", 0),
166
+ playing=d.get("playing", 0),
167
+ fps=round(d.get("fps", 0.0), 1),
168
+ ping=d.get("ping", 0),
169
+ )
170
+
171
+ def __str__(self) -> str:
172
+ fill = f"{self.playing}/{self.max_players}"
173
+ return f"[{self.id[:8]}…] Players: {fill} FPS: {self.fps} Ping: {self.ping}ms"
174
+
175
+
176
+ # ── Catalog ────────────────────────────────────────────────────────── #
177
+
178
+ @dataclass
179
+ class CatalogItem:
180
+ id: int
181
+ item_type: str
182
+ name: str
183
+ description: str
184
+ creator_name: str
185
+ creator_id: int
186
+ creator_type: str
187
+ price: Optional[int]
188
+ lowest_price: Optional[int]
189
+ purchase_count: int
190
+ favorite_count: int
191
+ asset_type: Optional[str] = None
192
+ bundle_type: Optional[str] = None
193
+ is_off_sale: bool = False
194
+
195
+ @classmethod
196
+ def from_dict(cls, d: dict) -> "CatalogItem":
197
+ return cls(
198
+ id=d.get("id", 0),
199
+ item_type=d.get("itemType", "Asset"),
200
+ name=d.get("name", ""),
201
+ description=d.get("description", ""),
202
+ creator_name=d.get("creatorName", ""),
203
+ creator_id=d.get("creatorTargetId", 0),
204
+ creator_type=d.get("creatorType", "User"),
205
+ price=d.get("price"),
206
+ lowest_price=d.get("lowestPrice"),
207
+ purchase_count=d.get("purchaseCount", 0),
208
+ favorite_count=d.get("favoriteCount", 0),
209
+ asset_type=d.get("assetType"),
210
+ bundle_type=d.get("bundleType"),
211
+ is_off_sale="OffSale" in d.get("itemStatus", []),
212
+ )
213
+
214
+ def __str__(self) -> str:
215
+ if self.price:
216
+ price_str = f"{self.price}R$"
217
+ elif self.is_off_sale:
218
+ price_str = "Off Sale"
219
+ else:
220
+ price_str = "Free"
221
+ return (
222
+ f"🛍 {self.name} [{self.item_type} #{self.id}]\n"
223
+ f" Creator : {self.creator_name}\n"
224
+ f" Price : {price_str}\n"
225
+ f" Sold : {self.purchase_count:,}\n"
226
+ f" Favorites: {self.favorite_count:,}"
227
+ )
228
+
229
+
230
+ @dataclass
231
+ class ResaleData:
232
+ asset_id: int
233
+ asset_stock: Optional[int]
234
+ sales: int
235
+ number_remaining: Optional[int]
236
+ recent_average_price: Optional[int]
237
+ original_price: Optional[int]
238
+
239
+ @classmethod
240
+ def from_dict(cls, asset_id: int, d: dict) -> "ResaleData":
241
+ return cls(
242
+ asset_id=asset_id,
243
+ asset_stock=d.get("assetStock"),
244
+ sales=d.get("sales", 0),
245
+ number_remaining=d.get("numberRemaining"),
246
+ recent_average_price=d.get("recentAveragePrice"),
247
+ original_price=d.get("originalPrice"),
248
+ )
249
+
250
+ def __str__(self) -> str:
251
+ rap = f"{self.recent_average_price:,}R$" if self.recent_average_price else "N/A"
252
+ return (
253
+ f"📈 Asset #{self.asset_id} Resale\n"
254
+ f" RAP : {rap}\n"
255
+ f" Sales : {self.sales:,}\n"
256
+ f" Remaining: {self.number_remaining}"
257
+ )
258
+
259
+
260
+ # ── Groups ─────────────────────────────────────────────────────────── #
261
+
262
+ @dataclass
263
+ class Group:
264
+ id: int
265
+ name: str
266
+ description: str
267
+ owner_id: Optional[int]
268
+ owner_name: Optional[str]
269
+ member_count: int
270
+ is_public: bool
271
+ has_verified_badge: bool = False
272
+
273
+ @classmethod
274
+ def from_dict(cls, d: dict) -> "Group":
275
+ owner = d.get("owner") or {}
276
+ return cls(
277
+ id=d.get("id", 0),
278
+ name=d.get("name", ""),
279
+ description=d.get("description", ""),
280
+ owner_id=owner.get("userId"),
281
+ owner_name=owner.get("username"),
282
+ member_count=d.get("memberCount", 0),
283
+ is_public=d.get("publicEntryAllowed", False),
284
+ has_verified_badge=d.get("hasVerifiedBadge", False),
285
+ )
286
+
287
+ def __str__(self) -> str:
288
+ verified = " ✓" if self.has_verified_badge else ""
289
+ pub = "Public" if self.is_public else "Private"
290
+ return (
291
+ f"👥 {self.name}{verified} [ID: {self.id}]\n"
292
+ f" Owner : {self.owner_name} (ID: {self.owner_id})\n"
293
+ f" Members: {self.member_count:,}\n"
294
+ f" Access : {pub}"
295
+ )
296
+
297
+
298
+ @dataclass
299
+ class GroupRole:
300
+ id: int
301
+ name: str
302
+ rank: int
303
+ member_count: int = 0
304
+
305
+ @classmethod
306
+ def from_dict(cls, d: dict) -> "GroupRole":
307
+ return cls(
308
+ id=d.get("id", 0),
309
+ name=d.get("name", ""),
310
+ rank=d.get("rank", 0),
311
+ member_count=d.get("memberCount", 0),
312
+ )
313
+
314
+ def __str__(self) -> str:
315
+ return f"[Rank {self.rank:>3}] {self.name:<30} ({self.member_count:,} members)"
316
+
317
+
318
+ # ── Friends ────────────────────────────────────────────────────────── #
319
+
320
+ @dataclass
321
+ class Friend:
322
+ id: int
323
+ name: str
324
+ display_name: str
325
+ is_online: bool = False
326
+ has_verified_badge: bool = False
327
+
328
+ @classmethod
329
+ def from_dict(cls, d: dict) -> "Friend":
330
+ return cls(
331
+ id=d.get("id", 0),
332
+ name=d.get("name", ""),
333
+ display_name=d.get("displayName", ""),
334
+ is_online=d.get("isOnline", False),
335
+ has_verified_badge=d.get("hasVerifiedBadge", False),
336
+ )
337
+
338
+ def __str__(self) -> str:
339
+ online = "🟢" if self.is_online else "⚫"
340
+ verified = " ✓" if self.has_verified_badge else ""
341
+ return f"{online} {self.display_name} (@{self.name}){verified} [ID: {self.id}]"
342
+
343
+
344
+ # ── Badges ─────────────────────────────────────────────────────────── #
345
+
346
+ @dataclass
347
+ class Badge:
348
+ id: int
349
+ name: str
350
+ description: str
351
+ enabled: bool
352
+ awarded_count: int = 0
353
+ win_rate_percentage: float = 0.0
354
+
355
+ @classmethod
356
+ def from_dict(cls, d: dict) -> "Badge":
357
+ stats = d.get("statistics", {})
358
+ return cls(
359
+ id=d.get("id", 0),
360
+ name=d.get("name", ""),
361
+ description=d.get("description", ""),
362
+ enabled=d.get("enabled", True),
363
+ awarded_count=stats.get("awardedCount", 0),
364
+ win_rate_percentage=stats.get("winRatePercentage", 0.0),
365
+ )
366
+
367
+ def __str__(self) -> str:
368
+ status = "✅" if self.enabled else "❌"
369
+ return (
370
+ f"{status} {self.name} [ID: {self.id}]\n"
371
+ f" Awarded : {self.awarded_count:,}\n"
372
+ f" Win Rate : {self.win_rate_percentage:.2f}%"
373
+ )
374
+
375
+
376
+ # ── Economy ────────────────────────────────────────────────────────── #
377
+
378
+ @dataclass
379
+ class RobuxBalance:
380
+ robux: int
381
+
382
+ def __str__(self) -> str:
383
+ return f"💎 {self.robux:,} Robux"
384
+
385
+
386
+ @dataclass
387
+ class Transaction:
388
+ id: int
389
+ created: str
390
+ is_pending: bool
391
+ agent_id: int
392
+ agent_name: str
393
+ agent_type: str
394
+ details_type: str
395
+ currency_type: str
396
+ amount: int
397
+
398
+ @classmethod
399
+ def from_dict(cls, d: dict) -> "Transaction":
400
+ agent = d.get("agent", {})
401
+ details = d.get("details", {})
402
+ currency = d.get("currency", {})
403
+ return cls(
404
+ id=d.get("id", 0),
405
+ created=d.get("created", ""),
406
+ is_pending=d.get("isPending", False),
407
+ agent_id=agent.get("id", 0),
408
+ agent_name=agent.get("name", ""),
409
+ agent_type=agent.get("type", ""),
410
+ details_type=details.get("type", ""),
411
+ currency_type=currency.get("type", ""),
412
+ amount=currency.get("amount", 0),
413
+ )
414
+
415
+ def __str__(self) -> str:
416
+ sign = "+" if self.amount > 0 else ""
417
+ pending = " (pending)" if self.is_pending else ""
418
+ return f"{sign}{self.amount}R$ from {self.agent_name} [{self.details_type}]{pending}"
419
+
420
+
421
+ # ── Avatar ─────────────────────────────────────────────────────────── #
422
+
423
+ @dataclass
424
+ class AvatarAsset:
425
+ id: int
426
+ name: str
427
+ asset_type_id: int
428
+ asset_type_name: str
429
+
430
+ @classmethod
431
+ def from_dict(cls, d: dict) -> "AvatarAsset":
432
+ at = d.get("assetType", {})
433
+ return cls(
434
+ id=d.get("id", 0),
435
+ name=d.get("name", ""),
436
+ asset_type_id=at.get("id", 0),
437
+ asset_type_name=at.get("name", ""),
438
+ )
439
+
440
+
441
+ @dataclass
442
+ class AvatarColors:
443
+ head: str
444
+ torso: str
445
+ right_arm: str
446
+ left_arm: str
447
+ right_leg: str
448
+ left_leg: str
449
+
450
+ @classmethod
451
+ def from_dict(cls, d: dict) -> "AvatarColors":
452
+ return cls(
453
+ head=str(d.get("headColorId", "")),
454
+ torso=str(d.get("torsoColorId", "")),
455
+ right_arm=str(d.get("rightArmColorId", "")),
456
+ left_arm=str(d.get("leftArmColorId", "")),
457
+ right_leg=str(d.get("rightLegColorId", "")),
458
+ left_leg=str(d.get("leftLegColorId", "")),
459
+ )
460
+
461
+
462
+ @dataclass
463
+ class Avatar:
464
+ user_id: int
465
+ avatar_type: str
466
+ scales: dict
467
+ assets: List[AvatarAsset] = field(default_factory=list)
468
+ colors: Optional[AvatarColors] = None
469
+
470
+ @classmethod
471
+ def from_dict(cls, user_id: int, d: dict) -> "Avatar":
472
+ return cls(
473
+ user_id=user_id,
474
+ avatar_type=d.get("playerAvatarType", ""),
475
+ scales=d.get("scales", {}),
476
+ assets=[AvatarAsset.from_dict(a) for a in d.get("assets", [])],
477
+ colors=AvatarColors.from_dict(d.get("bodyColors", {})),
478
+ )
479
+
480
+
481
+ # ── Pagination ─────────────────────────────────────────────────────── #
482
+
483
+ @dataclass
484
+ class Page:
485
+ """Generic paginated result from any list endpoint."""
486
+ data: list
487
+ next_cursor: Optional[str] = None
488
+ previous_cursor: Optional[str] = None
489
+
490
+ @classmethod
491
+ def from_dict(cls, d: dict, model=None) -> "Page":
492
+ raw = d.get("data", [])
493
+ items = [model.from_dict(i) for i in raw] if model else raw
494
+ return cls(
495
+ data=items,
496
+ next_cursor=d.get("nextPageCursor"),
497
+ previous_cursor=d.get("previousPageCursor"),
498
+ )
499
+
500
+ def __iter__(self):
501
+ return iter(self.data)
502
+
503
+ def __len__(self) -> int:
504
+ return len(self.data)
505
+
506
+ def __bool__(self) -> bool:
507
+ return bool(self.data)
508
+
509
+
510
+ __all__ = [
511
+ "User", "UserPresence",
512
+ "Game", "GameVotes", "GameServer",
513
+ "CatalogItem", "ResaleData",
514
+ "Group", "GroupRole",
515
+ "Friend",
516
+ "Badge",
517
+ "RobuxBalance", "Transaction",
518
+ "AvatarAsset", "AvatarColors", "Avatar",
519
+ "Page",
520
+ ]