-
Notifications
You must be signed in to change notification settings - Fork 46
catalog.roblox.com
endpoint support
#111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
58f0279
932c907
c47c6fc
f795f75
d814ef7
35fdb76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
""" | ||
|
||
This file contains the BaseCatalogItem object, which represents a Roblox catalog item ID. | ||
|
||
""" | ||
|
||
from __future__ import annotations | ||
from typing import TYPE_CHECKING | ||
|
||
from .baseitem import BaseItem | ||
|
||
if TYPE_CHECKING: | ||
from ..client import Client | ||
|
||
|
||
class BaseCatalogItem(BaseItem): | ||
""" | ||
Represents a catalog item ID. | ||
Instance IDs represent the ownership of a single Roblox item. | ||
|
||
Attributes: | ||
id: The item ID. | ||
item_type: The item's type, either 1 or 2. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make an enum for this: Asset or Bundle. |
||
""" | ||
|
||
def __init__(self, client: Client, catalog_item_id: int): | ||
""" | ||
Arguments: | ||
client: The Client this object belongs to. | ||
catalog_item_id: The ID of the catalog item. | ||
""" | ||
|
||
self._client: Client = client | ||
self.id: int = catalog_item_id | ||
self.item_type: int = catalog_item_type | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is this coming from? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was in the process of converting the |
||
|
||
# We need to redefine these special methods, as an asset and a bundle can have the same ID but not the same item_type | ||
def __repr__(self): | ||
return f"<{self.__class__.__name__} id={self.id} item_type={self.item_type}>" | ||
|
||
def __eq__(self, other): | ||
return isinstance(other, self.__class__) and (other.id == self.id) and (other.item_type == self.item_type) | ||
|
||
def __ne__(self, other): | ||
if isinstance(other, self.__class__): | ||
return (other.id != self.id) and (other.item_type != self.item_type) | ||
return True |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
""" | ||
|
||
This module contains classes intended to parse and deal with data from Roblox catalog endpoint. | ||
|
||
""" | ||
from __future__ import annotations | ||
from datetime import datetime | ||
from uuid import UUID | ||
from dateutil.parser import parse | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from .client import Client | ||
from typing import Optional, Union | ||
from .bases.basecatalogitem import BaseCatalogItem | ||
from .bases.baseuser import BaseUser | ||
from .assets import AssetType | ||
from .partials.partialgroup import PartialGroup | ||
from .partials.partialuser import CatalogCreatorPartialUser | ||
|
||
class CatalogItem(BaseCatalogItem): | ||
""" | ||
Represents a Catalog/Avatar Shop/Marketplace item. | ||
|
||
Attributes: | ||
id: The item's ID. | ||
name: The item's name. | ||
item_type: Unknown. | ||
asset_type: The asset's type as an instance of AssetType | ||
description: The item's description. | ||
is_offsale: If the item is offsale. | ||
creator: A class representing the creator of the item. | ||
price: The price of the item, in Robux. | ||
purchase_count: The number of times the item has been purchased. | ||
favorite_count: The number of times the item has been favorited. | ||
sale_location_type: Unknown. | ||
premium_pricing: A dictionary storing information about pricing for Roblox Premium members. | ||
premium_pricing.in_robux: The pricing for Roblox Premium members, in Robux. | ||
premium_pricing.discount_percentage: The percentage that Roblox Premium members get discounted. | ||
""" | ||
|
||
def __init__(self, client: Client, data: dict): | ||
self._client: Client = client | ||
self.id: int = data["id"] | ||
self.item_type = data["itemType"] | ||
super().__init__(client=self._client, catalog_item_id=self.id, catalog_item_type=self.item_type) | ||
|
||
self.name: str = data["name"] | ||
self.description: str = data["description"] | ||
|
||
self.asset_type: AssetType = AssetType(type_id=data["assetType"]) | ||
|
||
self.is_offsale: bool = data["isOffsale"] | ||
|
||
# Creator | ||
self.creator: Union[CatalogCreatorPartialUser, CatalogCreatorPartialGroup] | ||
if data["creatorType"] == "User": | ||
self.creator = CatalogCreatorPartialUser(client=client, data=data) | ||
elif data["creatorType"] == "Group": | ||
self.creator = CatalogCreatorPartialGroup(client=client, group_id=data) | ||
|
||
self.price: int = data["price"] | ||
self.purchase_count: int = data["purchaseCount"] | ||
self.favorite_count: int = data["favoriteCount"] | ||
self.sale_location_type: str = data["saleLocationType"] | ||
|
||
|
||
|
||
if data["premiumPricing"]: | ||
self.premium_pricing = {} | ||
self.premium_pricing.in_robux: int = data["premiumPricing"]["premiumPriceInRobux"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not how a dict works. also, why is this a dict and not a new class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙈 Clearly I'm spending too much time in Lua-land /lh Jokes aside: It was originally a class, but I forgot why I made it a dict. |
||
self.premium_pricing.discount_percentage: int = data["premiumPricing"]["premiumDiscountPercentage"] | ||
|
||
|
||
def __repr__(self): | ||
return f"<{self.__class__.__name__} name={self.name!r}>" | ||
|
||
|
||
class LimitedCatalogItem(CatalogItem): | ||
""" | ||
Represents a limited Catalog/Avatar Shop/Marketplace item. | ||
|
||
See also: | ||
CatalogItem, which this class inherits. | ||
|
||
Attributes: | ||
collectible_item_id: Unknown. | ||
quantity_limit_per_user: The maximum number of this item that a user can own. | ||
units_available_for_consumption: The amount of items that can be bought by all users. | ||
total_quantity: The amount of items that are owned or can be purchased. | ||
has_resellers: If the item has resellers. | ||
offsale_deadline: The time that an item goes offsale (as an instance of a datetime.datetime object). | ||
lowest_price: The lowest price, in Robux, offered to obtain this item. | ||
lowest_resale_price: The lowest resale price, in Robux, offered to obtain this item. | ||
price_status: Unknown. | ||
""" | ||
|
||
def __init__(self, client=client, data=data): | ||
super.__init__(client=client, data=data) | ||
|
||
self.collectible_item_id: UUID = UUID(data["collectibleItemId"]) | ||
self.quantity_limit_per_user: int = data["quantityLimitPerUser"] | ||
self.units_available_for_consumption: int = data["unitsAvailableForConsumption"] | ||
self.total_quantity: int = data["totalQuantity"] | ||
self.has_resellers: bool = data["hasResellers"] | ||
self.offsale_deadline: Optional[datetime] = parse(data["offsaleDeadline"]) | ||
self.lowest_price: int = data["lowestPrice"] | ||
self.lowest_resale_price: int = data["lowestResalePrice"] | ||
self.price_status: str = data["priceStatus"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
|
||
""" | ||
|
||
from typing import Union, List, Optional | ||
from typing import Union, List, Optional, Literal, TypedDict | ||
|
||
from .account import AccountProvider | ||
from .assets import EconomyAsset | ||
|
@@ -550,3 +550,47 @@ def get_base_gamepass(self, gamepass_id: int) -> BaseGamePass: | |
Returns: A BaseGamePass. | ||
""" | ||
return BaseGamePass(client=self, gamepass_id=gamepass_id) | ||
|
||
# Catalog | ||
def get_catalog_items(self, catalog_item_array: List[TypedDict[catalog_id: int, catalog_item_type: Literal[1, 2]]]) -> List[CatalogItem]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my first comment, that would address this TypedDict mess by allowing you to just pass BaseAssets or a new BaseBundle |
||
""" | ||
Gets a catalog item with the passed ID. | ||
|
||
The catalog is also known as the Avatar Shop or the Marketplace. | ||
|
||
Arguments: | ||
catalog_id: A Roblox catalog item ID. | ||
catalog_item_type: The type of item. 1 for an asset, and 2 for a bundle. | ||
|
||
Returns: | ||
A list of CatalogItem. | ||
""" | ||
try: | ||
catalog_item_response = await self._requests.post( | ||
url=self._url_generator.get_url( | ||
"catalog", "v1/catalog/items/details" | ||
), | ||
json={"data": catalog_item_array} | ||
) | ||
except NotFound as exception: | ||
raise CatalogItemNotFound( | ||
message="Invalid catalog item.", | ||
response=exception.response | ||
) from None | ||
catalog_item_data = catalog_item_response.json() | ||
catalog_list: Literal[CatalogItem] = [] | ||
for catalog_item in catalog_item_data: | ||
if data["collectibleItemId"]: # This is the only consistent indicator of an item's limited status | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are still "classic Limiteds" that are not collectibles, which makes this inaccurate. Either way, I think just ditch the two classes and go with one CatalogItem class for both. Makes more sense to me even if the Python type system might not be able to imply that multiple properties must exist if one does. If we end up keeping this, call it a CollectibleCatalogItem instead for accuracy. |
||
catalog_list.append(LimitedCatalogItem(client=self, data=catalog_item)) | ||
else: | ||
catalog_list.append(CatalogItem(client=self, data=catalog_item)) | ||
|
||
return catalog_list | ||
|
||
def get_base_catalog_items(self, catalog_item_array: List[TypedDict[catalog_id: int, catalog_item_type: Literal[1, 2]]]) -> List[CatalogItem]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditch the typeddict with the first comment's solutions, ditch the 1,2 for the enum |
||
catalog_list: Literal[CatalogItem] = [] | ||
|
||
for catalog_item in catalog_item_array: | ||
catalog_list.append(BaseCatalogItem(client=self, data=catalog_item)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for code quality and appearance, use a list comprehension here: |
||
|
||
return catalog_list |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,3 +79,25 @@ def __init__(self, client: Client, data: dict): | |
super().__init__(client=client, data=data) | ||
|
||
self.previous_usernames: List[str] = data["previousUsernames"] | ||
|
||
|
||
class CatalogCreatorPartialUser(PartialUser): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think a new class is warranted here. I think, unless it has extra fields, all PartialUser-shaped objects should just be PartialUsers. Maybe PartialUser could optionally take fields directly , like |
||
""" | ||
Represents a partial user in the context of a catalog item. | ||
Attributes: | ||
id: Id of the user. | ||
name: Name of the user. | ||
has_verified_badge: If the user has a verified badge. | ||
""" | ||
|
||
def __init__(self, client: Client, data: dict): | ||
""" | ||
Arguments: | ||
client: The Client. | ||
data: The data from the endpoint. | ||
""" | ||
super().__init__(client=client, data=data) | ||
|
||
self.has_verified_badge: bool = data["creatorHasVerifiedBadge"] | ||
self.id: int = data["creatorTargetId"] | ||
self.name: str = data["creatorName"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catalog items are either bundles or assets. This approach of one unified class makes it impossible to invoke methods that BaseAsset or a hypothetical BaseBundle would support even when applicable. Maybe forego this class entirely, introduce a BaseBundle (which we need anyway), and make "catalog items" just a
BaseAsset | BaseBundle
union? Or, instead of a union, both could derive from a new BaseCatalogItem class with a@property
that indicates what "type" of catalog item it is (asset or bundle) determined byisinstance(self, BaseAsset): "asset"
etc. not sure.