From 58f02797bb38f0e322766a7920d5aa73fb66cca1 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Fri, 22 Dec 2023 21:54:48 -0800 Subject: [PATCH 1/6] chore: bump Python to 3.8+ We have been using 3.8-exclusive stuff for a bit now. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 737095d8..7f1fc447 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ "Examples": "https://github.com/ro-py/ro.py/tree/main/examples", "Twitter": "https://twitter.com/jmkdev" }, - "python_requires": '>=3.7', + "python_requires": '>=3.8', "install_requires": [ "httpx>=0.21.0", "python-dateutil>=2.8.0" From 932c90709cacc51c34bb215eb95faeadd7eceff5 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:25:50 -0800 Subject: [PATCH 2/6] exceptions: add CatalogItemNotFound --- roblox/utilities/exceptions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/roblox/utilities/exceptions.py b/roblox/utilities/exceptions.py index 2ee2f0d5..a3a288e0 100644 --- a/roblox/utilities/exceptions.py +++ b/roblox/utilities/exceptions.py @@ -205,6 +205,13 @@ class GroupNotFound(ItemNotFound): pass +class CatalogItemNotFound(ItemNotFound): + """ + Raised for invalid catalog item IDs. + """ + pass + + class PlaceNotFound(ItemNotFound): """ Raised for invalid place IDs. From c47c6fcf54cbe10d4a82fdadd40973f807545c66 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:52:23 -0800 Subject: [PATCH 3/6] {partialgroup,partialuser}: Add Catalog partials --- roblox/partials/partialgroup.py | 31 +++++++++++++++++++++++++++++++ roblox/partials/partialuser.py | 22 ++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/roblox/partials/partialgroup.py b/roblox/partials/partialgroup.py index fa4574a7..6c58812b 100644 --- a/roblox/partials/partialgroup.py +++ b/roblox/partials/partialgroup.py @@ -72,3 +72,34 @@ def __init__(self, client: Client, data: dict): def __repr__(self): return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>" + + +class CatalogCreatorPartialGroup(BaseGroup): + """ + Represents a partial group in the context of a catalog item. + + Attributes: + _data: The data we get back from the endpoint. + _client: The client object, which is passed to all objects this client generates. + id: Id of the group + name: Name of the group + has_verified_badge: If the group has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The ClientSharedObject. + data: The data from the endpoint. + """ + self._client: Client = client + + super().__init__(client=client, data=data) + + self.has_verified_badge: bool = data["creatorHasVerifiedBadge"] + self.id: int = data["creatorTargetId"] + self.name: str = data["creatorName"] + + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>" diff --git a/roblox/partials/partialuser.py b/roblox/partials/partialuser.py index 17eaa974..5dff0a27 100644 --- a/roblox/partials/partialuser.py +++ b/roblox/partials/partialuser.py @@ -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): + """ + 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"] From f795f7533a7a988bc08fad43cbf6c038ad174e32 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:52:43 -0800 Subject: [PATCH 4/6] bases.basecatalogitem: Init --- roblox/bases/basecatalogitem.py | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 roblox/bases/basecatalogitem.py diff --git a/roblox/bases/basecatalogitem.py b/roblox/bases/basecatalogitem.py new file mode 100644 index 00000000..375ef1d0 --- /dev/null +++ b/roblox/bases/basecatalogitem.py @@ -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 Roblox instance 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. + """ + + 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 + + # 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 From d814ef70bb5080c5d4f1b82041b4440839198394 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:52:55 -0800 Subject: [PATCH 5/6] catalog: init --- roblox/catalog.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++ roblox/client.py | 46 +++++++++++++++++++- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 roblox/catalog.py diff --git a/roblox/catalog.py b/roblox/catalog.py new file mode 100644 index 00000000..0a1878fe --- /dev/null +++ b/roblox/catalog.py @@ -0,0 +1,108 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox catalog endpoint. + +""" +from __future__ import annotations +from datetime import datetime + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client + from typing import Optional +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: CatalogCreatorPartialUser or 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"] + 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: str = 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] = datetime.fromtimestamp(data["offsaleDeadline"]) + self.lowest_price: int = data["lowestPrice"] + self.lowest_resale_price: int = data["lowestResalePrice"] + self.price_status: str = data["priceStatus"] diff --git a/roblox/client.py b/roblox/client.py index fbea3a43..94cfe94e 100644 --- a/roblox/client.py +++ b/roblox/client.py @@ -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]: + """ + 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" + ), + data={"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 + 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]: + catalog_list: Literal[CatalogItem] = [] + + for catalog_item in catalog_item_array: + catalog_list.append(BaseCatalogItem(client=self, data=catalog_item)) + + return catalog_list From 35fdb760ffd87f7fe85a6d315abc94a516d92273 Mon Sep 17 00:00:00 2001 From: Tomo <68489118+tomodachi94@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:08:40 -0800 Subject: [PATCH 6/6] Preliminary fixes I fixed most of the things that won't require huge fixes. I still have a lot of work to do on this PR, but this is a start. --- roblox/bases/basecatalogitem.py | 2 +- roblox/catalog.py | 10 ++++++---- roblox/client.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/roblox/bases/basecatalogitem.py b/roblox/bases/basecatalogitem.py index 375ef1d0..b0eb612b 100644 --- a/roblox/bases/basecatalogitem.py +++ b/roblox/bases/basecatalogitem.py @@ -15,7 +15,7 @@ class BaseCatalogItem(BaseItem): """ - Represents a Roblox instance ID. + Represents a catalog item ID. Instance IDs represent the ownership of a single Roblox item. Attributes: diff --git a/roblox/catalog.py b/roblox/catalog.py index 0a1878fe..38ea3cd0 100644 --- a/roblox/catalog.py +++ b/roblox/catalog.py @@ -5,12 +5,14 @@ """ 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 + from typing import Optional, Union from .bases.basecatalogitem import BaseCatalogItem from .bases.baseuser import BaseUser from .assets import AssetType @@ -52,7 +54,7 @@ def __init__(self, client: Client, data: dict): self.is_offsale: bool = data["isOffsale"] # Creator - self.creator: CatalogCreatorPartialUser or CatalogCreatorPartialGroup + self.creator: Union[CatalogCreatorPartialUser, CatalogCreatorPartialGroup] if data["creatorType"] == "User": self.creator = CatalogCreatorPartialUser(client=client, data=data) elif data["creatorType"] == "Group": @@ -97,12 +99,12 @@ class LimitedCatalogItem(CatalogItem): def __init__(self, client=client, data=data): super.__init__(client=client, data=data) - self.collectible_item_id: str = data["collectibleItemId"] + 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] = datetime.fromtimestamp(data["offsaleDeadline"]) + 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"] diff --git a/roblox/client.py b/roblox/client.py index 94cfe94e..3b47264d 100644 --- a/roblox/client.py +++ b/roblox/client.py @@ -570,7 +570,7 @@ def get_catalog_items(self, catalog_item_array: List[TypedDict[catalog_id: int, url=self._url_generator.get_url( "catalog", "v1/catalog/items/details" ), - data={"data": catalog_item_array} + json={"data": catalog_item_array} ) except NotFound as exception: raise CatalogItemNotFound(