diff --git a/.gitignore b/.gitignore index b6e4761..38dee03 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Pycharm +.idea/ \ No newline at end of file diff --git a/open_sea_v1/__init__.py b/open_sea_v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/endpoints/__init__.py b/open_sea_v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/endpoints/endpoint_abc.py b/open_sea_v1/endpoints/endpoint_abc.py new file mode 100644 index 0000000..40cb88a --- /dev/null +++ b/open_sea_v1/endpoints/endpoint_abc.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from requests import Response + + +class BaseOpenSeaEndpoint(ABC): + + @abstractmethod + def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: + """Call to super().get_request passing url and _request_params.""" + + @property + @abstractmethod + def api_key(self) -> Optional[str]: + """Optional OpenSea API key""" + + @property + @abstractmethod + def url(self) -> str: + """Endpoint URL""" + + @property + @abstractmethod + def _request_params(self) -> dict: + """Dictionnary of _request_params to pass into the get_request.""" + + @property + @abstractmethod + def validate_request_params(self) -> None: + """""" + + @property + @abstractmethod + def response(self) -> list[dict]: + """Parsed JSON dictionnary from HTTP Response.""" diff --git a/open_sea_v1/endpoints/endpoint_assets.py b/open_sea_v1/endpoints/endpoint_assets.py new file mode 100644 index 0000000..7bc1b2a --- /dev/null +++ b/open_sea_v1/endpoints/endpoint_assets.py @@ -0,0 +1,145 @@ +from dataclasses import dataclass +from typing import Optional + +from requests import Response + +from open_sea_v1.endpoints.endpoint_enums import AssetsOrderBy, OpenseaApiEndpoints +from open_sea_v1.endpoints.endpoint_base_client import OpenSeaClient +from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint +from open_sea_v1.responses.asset_obj import Asset + + +@dataclass +class AssetsEndpoint(OpenSeaClient, BaseOpenSeaEndpoint): + """ + Opensea API Assets Endpoint + + Parameters + ---------- + width: + width of the snake + + owner: + The address of the owner of the assets + + token_ids: + List of token IDs to search for + + asset_contract_address: + The NFT contract address for the assets + + asset_contract_addresses: + List of contract addresses to search for. Will return a list of assets with contracts matching any of the addresses in this array. If "token_ids" is also specified, then it will only return assets that match each (address, token_id) pairing, respecting order. + + order_by: + How to order the assets returned. By default, the API returns the fastest ordering (contract address and token id). Options you can set are token_id, sale_date (the last sale's transaction's timestamp), sale_count (number of sales), visitor_count (number of unique visitors), and sale_price (the last sale's total_price) + + order_direction: + Can be asc for ascending or desc for descending + + offset: + Offset + + limit: + Defaults to 20, capped at 50. + + collection: + Limit responses to members of a collection. Case sensitive and must match the collection slug exactly. Will return all assets from all contracts in a collection. + + :return: Parsed JSON + """ + api_key: Optional[str] = None + owner: Optional[str] = None + token_ids: Optional[list[int]] = None + asset_contract_address: Optional[list[str]] = None + asset_contract_addresses: Optional[str] = None + collection: Optional[str] = None + order_by: Optional[AssetsOrderBy] = None + order_direction: str = None + offset: int = 0 + limit: int = 20 + + def __post_init__(self): + self.validate_request_params() + self._response: Optional[Response] = None + + @property + def http_response(self): + self._validate_response_property() + return self._response + + @property + def response(self) -> list[Asset]: + self._validate_response_property() + assets_json = self._response.json()['assets'] + assets = [Asset(asset_json) for asset_json in assets_json] + return assets + + @property + def url(self): + return OpenseaApiEndpoints.ASSETS.value + + def get_request(self, *args, **kwargs): + self._response = super().get_request(self.url, **self._request_params) + + @property + def _request_params(self) -> dict[dict]: + params = dict( + owner=self.owner, token_ids=self.token_ids, asset_contract_address=self.asset_contract_address, + asset_contract_addresses=self.asset_contract_addresses, collection=self.collection, + order_by=self.order_by, order_direction=self.order_direction, offset=self.offset, limit=self.limit + ) + return dict(api_key=self.api_key, params=params) + + def validate_request_params(self) -> None: + self._validate_mandatory_params() + self._validate_asset_contract_addresses() + self._validate_order_direction() + self._validate_order_by() + self._validate_limit() + + def _validate_response_property(self): + if self._response is None: + raise AttributeError('You must call self.request prior to accessing self.response') + + def _validate_mandatory_params(self): + mandatory = self.owner, self.token_ids, self.asset_contract_address, self.asset_contract_addresses, self.collection + if all((a is None for a in mandatory)): + raise ValueError("At least one of the following parameters must not be None:\n" + "owner, token_ids, asset_contract_address, asset_contract_addresses, collection") + + def _validate_asset_contract_addresses(self): + if self.asset_contract_address and self.asset_contract_addresses: + raise ValueError( + "You cannot simultaneously get_request for a single contract_address and a list of contract_addresses." + ) + + if self.token_ids and not (self.asset_contract_address or self.asset_contract_addresses): + raise ValueError( + "You cannot query for token_ids without specifying either " + "asset_contract_address or asset_contract_addresses." + ) + + def _validate_order_direction(self): + if self.order_direction is None: + return + + if self.order_direction not in ['asc', 'desc']: + raise ValueError( + f"order_direction param value ({self.order_direction}) is invalid. " + f"Must be either 'asc' or 'desc', case sensitive." + ) + + def _validate_order_by(self) -> None: + if self.order_by is None: + return + + if self.order_by not in AssetsOrderBy.list(): + raise ValueError( + f"order_by param value ({self.order_by}) is invalid. " + f"Must be a value from {AssetsOrderBy.list()}, case sensitive." + ) + + def _validate_limit(self): + if not isinstance(self.limit, int) or not 0 <= self.limit <= 50: + raise ValueError(f"limit param must be an int between 0 and 50.") diff --git a/open_sea_v1/endpoints/endpoint_base_client.py b/open_sea_v1/endpoints/endpoint_base_client.py new file mode 100644 index 0000000..b74e999 --- /dev/null +++ b/open_sea_v1/endpoints/endpoint_base_client.py @@ -0,0 +1,63 @@ +from requests import Response, request + + +class OpenSeaClient: + + api_key = None + + @property + def http_headers(self) -> dict: + return { + "headers": + {"X-API-Key" : self.api_key} if self.api_key else dict(), + } + + def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: + """ + Automatically passes in API key in HTTP get_request headers. + """ + if 'api_key' in kwargs: + self.api_key = kwargs.pop('api_key') + updated_kwargs = kwargs | self.http_headers + return request(method, url, **updated_kwargs) + + # def collections(self, *, asset_owner: Optional[str] = None, offset: int, limit: int) -> OpenseaCollections: + # """ + # Use this endpoint to fetch collections and dapps that OpenSea shows on opensea.io, + # along with dapps and smart contracts that a particular user cares about. + # + # :param asset_owner: A wallet address. If specified, will return collections where + # the owner owns at least one asset belonging to smart contracts in the collection. + # The number of assets the account owns is shown as owned_asset_count for each collection. + # :param offset: For pagination. Number of contracts offset from the beginning of the result list. + # :param limit: For pagination. Maximum number of contracts to return. + # :return: Parsed JSON + # """ + # if offset != 0: + # raise NotImplementedError( + # "Sorry, tested offset parameter is not implemented yet. " + # "Feel free to PR after looking at the tests and trying to understand" + # " why current implementation doesn't allow pagination to work..." + # ) + # resp = self._collections(asset_owner=asset_owner, offset=offset, limit=limit) + # return resp.json()['collections'] + # + # def _collections(self, **_request_params) -> Response: + # """Returns HTTPResponse object.""" + # url = OpenseaApiEndpoints.COLLECTIONS.value + # return self.get_request("GET", url, _request_params=_request_params) + # + # def asset(self, asset_contract_address: str, token_id: str, account_address: Optional[str] = None) -> OpenseaAsset: + # """ + # :param asset_contract_address: Address of the contract for this NFT + # :param token_id: Token ID for this item + # :param account_address: Address of an owner of the token. If you include this, the http_response will include an ownership object that includes the number of tokens owned by the address provided instead of the top_ownerships object included in the standard http_response, which provides the number of tokens owned by each of the 10 addresses with the greatest supply of the token. + # :return: Parsed JSON. + # """ + # resp = self._asset( + # asset_contract_address=asset_contract_address, + # token_id=token_id, + # account_address=account_address, + # ) + # return resp.response() + diff --git a/open_sea_v1/endpoints/endpoint_enums.py b/open_sea_v1/endpoints/endpoint_enums.py new file mode 100644 index 0000000..478eb68 --- /dev/null +++ b/open_sea_v1/endpoints/endpoint_enums.py @@ -0,0 +1,94 @@ +from enum import Enum + +OPENSEA_API_V1 = "https://api.opensea.io/api/v1/" +OPENSEA_LISTINGS_V1 = "https://api.opensea.io/wyvern/v1/" + + +class ExtendedStrEnum(str, Enum): + + @classmethod + def list(cls) -> list[str]: + return list(map(lambda c: c.value, cls)) + + +class OpenseaApiEndpoints(ExtendedStrEnum): + ASSET = OPENSEA_API_V1 + "asset" + ASSETS = OPENSEA_API_V1 + "assets" + ASSET_CONTRACT = OPENSEA_API_V1 + "asset_contract" + BUNDLES = OPENSEA_API_V1 + "bundles" + EVENTS = OPENSEA_API_V1 + "events" + COLLECTIONS = OPENSEA_API_V1 + "collections" + LISTINGS = OPENSEA_LISTINGS_V1 + "orders" + + +class AssetsOrderBy(ExtendedStrEnum): + TOKEN_ID = "token_id" + SALE_DATE = "sale_date" + SALE_COUNT = "sale_count" + VISITOR_COUNT = "visitor_count" + SALE_PRICE = "sale_price" + + +class Asset(ExtendedStrEnum): + TOKEN_ID = "token_id" + NUM_SALES = "num_sales" + BACKGROUND_COLOR = "background_color" + IMAGE_URL = "image_url" + IMAGE_PREVIEW_URL = "image_preview_url" + IMAGE_THUMBNAIL_URL = "image_thumbnail_url" + IMAGE_ORIGINAL_URL = "image_original_url" + ANIMATION_URL = "animation_url" + ANIMATION_ORIGINAL_URL = "animation_original_url" + NAME = "name" + DESCRIPTION = "description" + EXTERNAL_LINK = "external_link" + ASSET_CONTRACT_DICT = "asset_contract" + PERMALINK = "permalink" + COLLECTION_DICT = "collection" + DECIMALS = "decimals" + TOKEN_METADATA = "token_metadata" + OWNER_DICT = "owner" + SELL_ORDERS = "sell_orders" + CREATOR_DICT = "creator" + TRAITS_DICT = "traits" + LAST_SALE_DICT = "last_sale" + TOP_BID = "top_bid" + LISTING_DATE = "listing_date" + IS_PRESALE = "is_presale" + TRANSFER_FEE_PAYMENT_TOKEN = "transfer_fee_payment_token" + TRANSFER_FEE = "transfer_fee" + + +class AssetTraits(ExtendedStrEnum): + TRAIT_TYPE = "trait_type" + VALUE = "value" + DISPLAY_TYPE = "display_type" + + +class AssetContract(ExtendedStrEnum): + ADDRESS = "address" + NAME = "name" + SYMBOL = "symbol" + IMAGE_URL = "image_url" + DESCRIPTION = "description" + EXTERNAL_LINK = "external_link" + + +class AssetOwner(ExtendedStrEnum): + ADDRESS = 'address' + CONFIG = 'config' + PROFILE_IMG_URL = 'profile_img_url' + USER = 'user' + + +class AssetLastSale(ExtendedStrEnum): + ASSET = 'asset' + ASSET_BUNDLE = 'asset_bundle' + EVENT_TYPE = 'event_type' + EVENT_TIMESTAMP = 'event_timestamp' + AUCTION_TYPE = 'auction_type' + TOTAL_PRICE = 'total_price' + PAYMENT_TOKEN_DICT = 'payment_token' + TRANSACTION_DICT = 'transaction' + CREATED_DATE = 'created_date' + QUANTITY = 'quantity' \ No newline at end of file diff --git a/open_sea_v1/endpoints/tests/__init__.py b/open_sea_v1/endpoints/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/endpoints/tests/test_assets.py b/open_sea_v1/endpoints/tests/test_assets.py new file mode 100644 index 0000000..59d5a53 --- /dev/null +++ b/open_sea_v1/endpoints/tests/test_assets.py @@ -0,0 +1,75 @@ +from itertools import combinations +from unittest import TestCase + +from open_sea_v1.endpoints.endpoint_assets import AssetsEndpoint +from open_sea_v1.endpoints.endpoint_enums import AssetsOrderBy +from open_sea_v1.responses.asset_obj import Asset + + +class TestAssetsRequest(TestCase): + sample_contract = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" # punk + sample_wallet = "0x5ca12f79e4d33b0bd153b40df59f6db9ee03482e" # punk + default_asset_params = dict(token_ids=[5, 6, 7], asset_contract_address=sample_contract) + + @staticmethod + def create_and_get(**kwargs) -> list[Asset]: + """Shortcut""" + client = AssetsEndpoint(**kwargs) + client.get_request() + return client.response + + def test_cannot_all_be_none_owner_token_ids_asset_contract_address_asset_contract_addresses_collection(self): + assets_kwargs = dict(owner=0, token_ids=0, collection=0, asset_contract_address=0, asset_contract_addresses=0) + for kwarg_combo in combinations(assets_kwargs, r=len(assets_kwargs)): + self.assertRaises(ValueError, AssetsEndpoint, kwarg_combo) + + def test_param_owner_returns_assets_from_specified_owner(self): + params = dict(owner=self.sample_wallet, order_direction='asc', **self.default_asset_params) + for punk in self.create_and_get(**params): + self.assertEqual(punk.owner.address, self.sample_wallet) + + def test_param_token_ids_raise_exception_if_missing_contract_address_and_addresses(self): + self.assertRaises(ValueError, AssetsEndpoint, token_ids=[1, 2, 3]) + + def test_params_cannot_be_simultaneously_be_passed_asset_contract_address_and_contract_addresses(self): + params = self.default_asset_params | dict(asset_contract_address=True, asset_contract_addresses=True) + self.assertRaises(ValueError, AssetsEndpoint, **params) + + def test_param_token_ids_returns_assets_corresponding_to_single_contract(self): + params = dict(order_direction='asc', **self.default_asset_params) + for punk in self.create_and_get(**params): + self.assertEqual(punk.asset_contract.address, self.sample_contract) + + def test_param_order_direction_can_only_be_asc_or_desc(self): + invalid_order_values = (False, 0, 1, "", [], (), {}, 'hi') + for invalid_order in invalid_order_values: + params = dict(token_ids=[1], asset_contract_address=self.sample_contract, order_direction=invalid_order) + self.assertRaises((ValueError, TypeError), AssetsEndpoint, **params) + + def test_param_order_by_token_id(self): + params = self.default_asset_params | dict(token_ids=[3, 2, 1], order_by=AssetsOrderBy.TOKEN_ID, order_direction='desc') + punks_ids = [punk.token_id for punk in self.create_and_get(**params)] + self.assertEqual(['3', '2', '1'], punks_ids) + + def test_param_order_by_sale_date(self): + params = self.default_asset_params | dict(token_ids=[1, 14, 33], order_by=AssetsOrderBy.SALE_DATE) + punks_sales = [punk.last_sale.event_timestamp for punk in self.create_and_get(**params)] + self.assertEqual(sorted(punks_sales, reverse=True), punks_sales) + + def test_param_order_by_sale_count(self): + params = self.default_asset_params | dict(token_ids=[1, 14, 33], order_by=AssetsOrderBy.SALE_COUNT) + punks_sales_cnt = [punk.num_sales for punk in self.create_and_get(**params)] + self.assertEqual(sorted(punks_sales_cnt, reverse=True), punks_sales_cnt) + + def test_param_order_by_sale_price(self): + params = self.default_asset_params | dict(token_ids=[1, 14, 33], order_by=AssetsOrderBy.SALE_PRICE) + punks_last_sale_price = [punk.last_sale.total_price for punk in self.create_and_get(**params)] + self.assertEqual(sorted(punks_last_sale_price, reverse=True), punks_last_sale_price) + + def test_param_order_by_visitor_count(self): + pass # as far as I know this is not returned in the API http_response and done directly by OpenSea + + def test_param_limit_cannot_be_below_0_or_above_50(self): + self.assertRaises(ValueError, AssetsEndpoint, limit="25", **self.default_asset_params) + self.assertRaises(ValueError, AssetsEndpoint, limit=-1, **self.default_asset_params) + self.assertRaises(ValueError, AssetsEndpoint, limit=51, **self.default_asset_params) \ No newline at end of file diff --git a/open_sea_v1/endpoints/tests/test_collection.py b/open_sea_v1/endpoints/tests/test_collection.py new file mode 100644 index 0000000..19f11f6 --- /dev/null +++ b/open_sea_v1/endpoints/tests/test_collection.py @@ -0,0 +1,30 @@ +# from unittest import TestCase +# +# from open_sea_v1.endpoints.endpoint_base_client import OpenSeaClient +# +# +# class TestCollectionsRequests(TestCase): +# client = CLIENT +# collections_limit = 7 +# collections_offset = 0 +# collections = client.collections(offset=collections_offset, limit=collections_limit) +# collection = collections[0] # default resp +# +# def test_collections_request_returns_valid_response(self): +# self.assertTrue(self.collection) +# +# def test_collections_request_limit_param(self): +# self.assertEqual(len(self.collections), self.collections_limit) +# +# def test_collections_request_offset_param(self): +# raise NotImplementedError() +# offset_collections = self.client.collections(limit=self.collections_limit, offset=self.collections_offset + 1) +# self.assertNotEqual(self.collections[0]['name'], offset_collections[0]['name']) +# self.assertNotEqual(self.collections[-1]['name'], offset_collections[-1]['name']) +# self.assertEqual(self.collections[-2]['name'], offset_collections[-1]['name']) +# self.assertEqual(self.collections[-2]['name'], offset_collections[-1]['name']) +# +# def test_collections_api_key_http_header_was_passed(self): +# api_client = OpenSeaClient(api_key='randomstuff') +# resp = api_client._collections(limit=1, offset=0) +# self.assertIn('X-API-Key', dict(resp.request.headers)) \ No newline at end of file diff --git a/open_sea_v1/endpoints/tests/test_enums.py b/open_sea_v1/endpoints/tests/test_enums.py new file mode 100644 index 0000000..aacd8bb --- /dev/null +++ b/open_sea_v1/endpoints/tests/test_enums.py @@ -0,0 +1,22 @@ +from unittest import TestCase + +import requests + +from open_sea_v1.endpoints.endpoint_enums import OpenseaApiEndpoints, ExtendedStrEnum + + +class TestExtendedStrEnum(TestCase): + + def test_extended_str_enum_class_method_list_values_returns_all_values(self): + class TestEnum(ExtendedStrEnum): + VALUE_1 = "value_1" + VALUE_2 = "value_2" + self.assertTrue(TestEnum.list(), ["value_1", "value_2"]) + + +class TestAPIEndpoints(TestCase): + + def test_endpoints_urls_responds_with_a_json(self): + for url in OpenseaApiEndpoints: + resp = requests.get(url.value).json() + self.assertTrue(resp, f"JSON Reponse was empty for {resp}") diff --git a/open_sea_v1/responses/__init__.py b/open_sea_v1/responses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/responses/asset_obj.py b/open_sea_v1/responses/asset_obj.py new file mode 100644 index 0000000..4ae9202 --- /dev/null +++ b/open_sea_v1/responses/asset_obj.py @@ -0,0 +1,127 @@ +""" +Assigns attributes to dictionnary values for easier object navigation. +""" +from dataclasses import dataclass + +from open_sea_v1.responses.collection_obj import _Collection + + +@dataclass +class _LastSale: + _last_sale: dict + + def __str__(self) -> str: + return f"({_LastSale.__name__}, asset={self.asset}, date={self.event_timestamp}, quantity={self.quantity})" + + def __post_init__(self): + self.asset: dict = self._last_sale['asset'] + self.asset_bundle = self._last_sale['asset_bundle'] + self.event_type = self._last_sale['event_type'] + self.event_timestamp = self._last_sale['event_timestamp'] + self.auction_type = self._last_sale['auction_type'] + self.total_price = self._last_sale['total_price'] + self.created_date = self._last_sale['created_date'] + self.quantity = self._last_sale['quantity'] + + @property + def transaction(self) -> dict: + return self._last_sale['transaction'] + + @property + def payment_token(self) -> dict: + return self._last_sale['payment_token'] + + +@dataclass +class _Traits: + _traits: dict + + def __post_init__(self): + self.trait_type = self._traits['trait_type'] + self.value = self._traits['value'] + self.display_type = self._traits['display_type'] + + +@dataclass +class _Owner: + _owner: dict + + def __str__(self) -> str: + return f"({_Owner.__name__}, user={self.user['username']})" + + def __post_init__(self): + self.address = self._owner['address'] + self.config = self._owner['config'] + self.profile_img_url = self._owner['profile_img_url'] + self.user: dict = self._owner['user'] + + +@dataclass +class _Contract: + _contract: dict + + def __str__(self) -> str: + return f"({_Contract.__name__} - {self.name.title()}: {self.description})" + + def __post_init__(self): + self.address = self._contract['address'] + self.name = self._contract['name'] + self.symbol = self._contract['symbol'] + self.image_url = self._contract['image_url'] + self.description = self._contract['description'] + self.external_link = self._contract['external_link'] + + +@dataclass +class Asset: + _json: dict + + def __str__(self) -> str: + return f"({Asset.__name__}, id={self.token_id.zfill(5)}, name={self.name})" + + def __post_init__(self): + self.token_id = self._json["token_id"] + self.num_sales = self._json["num_sales"] + self.background_color = self._json["background_color"] + self.image_url = self._json["image_url"] + self.image_preview_url = self._json["image_preview_url"] + self.image_thumbnail_url = self._json["image_thumbnail_url"] + self.image_original_url = self._json["image_original_url"] + self.animation_url = self._json["animation_url"] + self.animation_original_url = self._json["animation_original_url"] + self.name = self._json["name"] + self.description = self._json["description"] + self.external_link = self._json["external_link"] + self.permalink = self._json["permalink"] + self.decimals = self._json["decimals"] + self.token_metadata = self._json["token_metadata"] + self.sell_orders = self._json["sell_orders"] + self.top_bid = self._json["top_bid"] + self.listing_date = self._json["listing_date"] + self.is_presale = self._json["is_presale"] + self.transfer_fee_payment_token = self._json["transfer_fee_payment_token"] + self.transfer_fee = self._json["transfer_fee"] + + @property + def asset_contract(self) -> _Contract: + return _Contract(self._json['asset_contract']) + + @property + def owner(self) -> _Owner: + return _Owner(self._json['owner']) + + @property + def traits(self) -> _Traits: + return _Traits(self._json['traits']) + + @property + def last_sale(self) -> _LastSale: + return _LastSale(self._json['last_sale']) + + @property + def collection(self): + return _Collection(self._json['collection']) + + @property + def creator(self) -> dict: + raise self._json['creator'] diff --git a/open_sea_v1/responses/collection_obj.py b/open_sea_v1/responses/collection_obj.py new file mode 100644 index 0000000..2bdd28a --- /dev/null +++ b/open_sea_v1/responses/collection_obj.py @@ -0,0 +1,81 @@ +""" +Assigns attributes to dictionnary values for easier object navigation. +""" +from dataclasses import dataclass + + +@dataclass +class _CollectionStats: + _json: dict + + def __str__(self) -> str: + return f"({_CollectionStats.__name__}, {self.floor_price=}, {self.average_price=}, {self.market_cap=})" + + def __post_init__(self): + self.one_day_volume = self._json["one_day_volume"] + self.one_day_change = self._json["one_day_change"] + self.one_day_average_price = self._json["one_day_average_price"] + self.one_day_sales = self._json["one_day_sales"] + self.seven_day_volume = self._json["seven_day_volume"] + self.seven_day_change = self._json["seven_day_change"] + self.seven_day_sales = self._json["seven_day_sales"] + self.seven_day_average_price = self._json["seven_day_average_price"] + self.thirty_day_volume = self._json["thirty_day_volume"] + self.thirty_day_change = self._json["thirty_day_change"] + self.thirty_day_sales = self._json["thirty_day_sales"] + self.thirty_day_average_price = self._json["thirty_day_average_price"] + self.total_volume = self._json["total_volume"] + self.total_sales = self._json["total_sales"] + self.total_supply = self._json["total_supply"] + self.count = self._json["count"] + self.num_owners = self._json["num_owners"] + self.average_price = self._json["average_price"] + self.num_reports = self._json["num_reports"] + self.market_cap = self._json["market_cap"] + self.floor_price = self._json["floor_price"] + + +@dataclass +class _Collection: + _json: dict + + def __str__(self) -> str: + return f"({_Collection.__name__}, {self.name=}, {self.short_description=})" + + def __post_init__(self): + self.primary_asset_contracts = self._json["primary_asset_contracts"] + self.traits = self._json["traits"] + self.banner_image_url = self._json["banner_image_url"] + self.chat_url = self._json["chat_url"] + self.created_date = self._json["created_date"] + self.default_to_fiat = self._json["default_to_fiat"] + self.description = self._json["description"] + self.dev_buyer_fee_basis_points = self._json["dev_buyer_fee_basis_points"] + self.dev_seller_fee_basis_points = self._json["dev_seller_fee_basis_points"] + self.discord_url = self._json["discord_url"] + self.display_data = self._json["display_data"] + self.external_url = self._json["external_url"] + self.featured = self._json["featured"] + self.featured_image_url = self._json["featured_image_url"] + self.hidden = self._json["hidden"] + self.safelist_request_status = self._json["safelist_request_status"] + self.image_url = self._json["image_url"] + self.is_subject_to_whitelist = self._json["is_subject_to_whitelist"] + self.large_image_url = self._json["large_image_url"] + self.medium_username = self._json["medium_username"] + self.only_proxied_transfers = self._json["only_proxied_transfers"] + self.opensea_buyer_fee_basis_points = self._json["opensea_buyer_fee_basis_points"] + self.opensea_seller_fee_basis_points = self._json["opensea_seller_fee_basis_points"] + self.payout_address = self._json["payout_address"] + self.require_email = self._json["require_email"] + self.short_description = self._json["short_description"] + self.slug = self._json["slug"] + self.telegram_url = self._json["telegram_url"] + self.twitter_username = self._json["twitter_username"] + self.instagram_username = self._json["instagram_username"] + self.wiki_url = self._json["wiki_url"] + self.name = self._json["name"] + + @property + def stats(self) -> _CollectionStats: + return _CollectionStats(self._json['stats']) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file