Skip to content

Commit 9bb4f9d

Browse files
committed
Implemented Assets Endpoint class
1 parent a02fc1a commit 9bb4f9d

File tree

15 files changed

+677
-0
lines changed

15 files changed

+677
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,6 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# Pycharm
132+
.idea/

open_sea_v1/__init__.py

Whitespace-only changes.

open_sea_v1/endpoints/__init__.py

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Optional
3+
4+
from requests import Response
5+
6+
7+
class BaseOpenSeaEndpoint(ABC):
8+
9+
@abstractmethod
10+
def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response:
11+
"""Call to super().get_request passing url and _request_params."""
12+
13+
@property
14+
@abstractmethod
15+
def api_key(self) -> Optional[str]:
16+
"""Optional OpenSea API key"""
17+
18+
@property
19+
@abstractmethod
20+
def url(self) -> str:
21+
"""Endpoint URL"""
22+
23+
@property
24+
@abstractmethod
25+
def _request_params(self) -> dict:
26+
"""Dictionnary of _request_params to pass into the get_request."""
27+
28+
@property
29+
@abstractmethod
30+
def validate_request_params(self) -> None:
31+
""""""
32+
33+
@property
34+
@abstractmethod
35+
def response(self) -> list[dict]:
36+
"""Parsed JSON dictionnary from HTTP Response."""
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from requests import Response
5+
6+
from open_sea_v1.endpoints.endpoint_enums import AssetsOrderBy, OpenseaApiEndpoints
7+
from open_sea_v1.endpoints.endpoint_base_client import OpenSeaClient
8+
from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint
9+
from open_sea_v1.responses.asset_obj import Asset
10+
11+
12+
@dataclass
13+
class AssetsEndpoint(OpenSeaClient, BaseOpenSeaEndpoint):
14+
"""
15+
Opensea API Assets Endpoint
16+
17+
Parameters
18+
----------
19+
width:
20+
width of the snake
21+
22+
owner:
23+
The address of the owner of the assets
24+
25+
token_ids:
26+
List of token IDs to search for
27+
28+
asset_contract_address:
29+
The NFT contract address for the assets
30+
31+
asset_contract_addresses:
32+
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.
33+
34+
order_by:
35+
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)
36+
37+
order_direction:
38+
Can be asc for ascending or desc for descending
39+
40+
offset:
41+
Offset
42+
43+
limit:
44+
Defaults to 20, capped at 50.
45+
46+
collection:
47+
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.
48+
49+
:return: Parsed JSON
50+
"""
51+
api_key: Optional[str] = None
52+
owner: Optional[str] = None
53+
token_ids: Optional[list[int]] = None
54+
asset_contract_address: Optional[list[str]] = None
55+
asset_contract_addresses: Optional[str] = None
56+
collection: Optional[str] = None
57+
order_by: Optional[AssetsOrderBy] = None
58+
order_direction: str = None
59+
offset: int = 0
60+
limit: int = 20
61+
62+
def __post_init__(self):
63+
self.validate_request_params()
64+
self._response: Optional[Response] = None
65+
66+
@property
67+
def http_response(self):
68+
self._validate_response_property()
69+
return self._response
70+
71+
@property
72+
def response(self) -> list[Asset]:
73+
self._validate_response_property()
74+
assets_json = self._response.json()['assets']
75+
assets = [Asset(asset_json) for asset_json in assets_json]
76+
return assets
77+
78+
@property
79+
def url(self):
80+
return OpenseaApiEndpoints.ASSETS.value
81+
82+
def get_request(self, *args, **kwargs):
83+
self._response = super().get_request(self.url, **self._request_params)
84+
85+
@property
86+
def _request_params(self) -> dict[dict]:
87+
params = dict(
88+
owner=self.owner, token_ids=self.token_ids, asset_contract_address=self.asset_contract_address,
89+
asset_contract_addresses=self.asset_contract_addresses, collection=self.collection,
90+
order_by=self.order_by, order_direction=self.order_direction, offset=self.offset, limit=self.limit
91+
)
92+
return dict(api_key=self.api_key, params=params)
93+
94+
def validate_request_params(self) -> None:
95+
self._validate_mandatory_params()
96+
self._validate_asset_contract_addresses()
97+
self._validate_order_direction()
98+
self._validate_order_by()
99+
self._validate_limit()
100+
101+
def _validate_response_property(self):
102+
if self._response is None:
103+
raise AttributeError('You must call self.request prior to accessing self.response')
104+
105+
def _validate_mandatory_params(self):
106+
mandatory = self.owner, self.token_ids, self.asset_contract_address, self.asset_contract_addresses, self.collection
107+
if all((a is None for a in mandatory)):
108+
raise ValueError("At least one of the following parameters must not be None:\n"
109+
"owner, token_ids, asset_contract_address, asset_contract_addresses, collection")
110+
111+
def _validate_asset_contract_addresses(self):
112+
if self.asset_contract_address and self.asset_contract_addresses:
113+
raise ValueError(
114+
"You cannot simultaneously get_request for a single contract_address and a list of contract_addresses."
115+
)
116+
117+
if self.token_ids and not (self.asset_contract_address or self.asset_contract_addresses):
118+
raise ValueError(
119+
"You cannot query for token_ids without specifying either "
120+
"asset_contract_address or asset_contract_addresses."
121+
)
122+
123+
def _validate_order_direction(self):
124+
if self.order_direction is None:
125+
return
126+
127+
if self.order_direction not in ['asc', 'desc']:
128+
raise ValueError(
129+
f"order_direction param value ({self.order_direction}) is invalid. "
130+
f"Must be either 'asc' or 'desc', case sensitive."
131+
)
132+
133+
def _validate_order_by(self) -> None:
134+
if self.order_by is None:
135+
return
136+
137+
if self.order_by not in AssetsOrderBy.list():
138+
raise ValueError(
139+
f"order_by param value ({self.order_by}) is invalid. "
140+
f"Must be a value from {AssetsOrderBy.list()}, case sensitive."
141+
)
142+
143+
def _validate_limit(self):
144+
if not isinstance(self.limit, int) or not 0 <= self.limit <= 50:
145+
raise ValueError(f"limit param must be an int between 0 and 50.")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from requests import Response, request
2+
3+
4+
class OpenSeaClient:
5+
6+
api_key = None
7+
8+
@property
9+
def http_headers(self) -> dict:
10+
return {
11+
"headers":
12+
{"X-API-Key" : self.api_key} if self.api_key else dict(),
13+
}
14+
15+
def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response:
16+
"""
17+
Automatically passes in API key in HTTP get_request headers.
18+
"""
19+
if 'api_key' in kwargs:
20+
self.api_key = kwargs.pop('api_key')
21+
updated_kwargs = kwargs | self.http_headers
22+
return request(method, url, **updated_kwargs)
23+
24+
# def collections(self, *, asset_owner: Optional[str] = None, offset: int, limit: int) -> OpenseaCollections:
25+
# """
26+
# Use this endpoint to fetch collections and dapps that OpenSea shows on opensea.io,
27+
# along with dapps and smart contracts that a particular user cares about.
28+
#
29+
# :param asset_owner: A wallet address. If specified, will return collections where
30+
# the owner owns at least one asset belonging to smart contracts in the collection.
31+
# The number of assets the account owns is shown as owned_asset_count for each collection.
32+
# :param offset: For pagination. Number of contracts offset from the beginning of the result list.
33+
# :param limit: For pagination. Maximum number of contracts to return.
34+
# :return: Parsed JSON
35+
# """
36+
# if offset != 0:
37+
# raise NotImplementedError(
38+
# "Sorry, tested offset parameter is not implemented yet. "
39+
# "Feel free to PR after looking at the tests and trying to understand"
40+
# " why current implementation doesn't allow pagination to work..."
41+
# )
42+
# resp = self._collections(asset_owner=asset_owner, offset=offset, limit=limit)
43+
# return resp.json()['collections']
44+
#
45+
# def _collections(self, **_request_params) -> Response:
46+
# """Returns HTTPResponse object."""
47+
# url = OpenseaApiEndpoints.COLLECTIONS.value
48+
# return self.get_request("GET", url, _request_params=_request_params)
49+
#
50+
# def asset(self, asset_contract_address: str, token_id: str, account_address: Optional[str] = None) -> OpenseaAsset:
51+
# """
52+
# :param asset_contract_address: Address of the contract for this NFT
53+
# :param token_id: Token ID for this item
54+
# :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.
55+
# :return: Parsed JSON.
56+
# """
57+
# resp = self._asset(
58+
# asset_contract_address=asset_contract_address,
59+
# token_id=token_id,
60+
# account_address=account_address,
61+
# )
62+
# return resp.response()
63+
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from enum import Enum
2+
3+
OPENSEA_API_V1 = "https://api.opensea.io/api/v1/"
4+
OPENSEA_LISTINGS_V1 = "https://api.opensea.io/wyvern/v1/"
5+
6+
7+
class ExtendedStrEnum(str, Enum):
8+
9+
@classmethod
10+
def list(cls) -> list[str]:
11+
return list(map(lambda c: c.value, cls))
12+
13+
14+
class OpenseaApiEndpoints(ExtendedStrEnum):
15+
ASSET = OPENSEA_API_V1 + "asset"
16+
ASSETS = OPENSEA_API_V1 + "assets"
17+
ASSET_CONTRACT = OPENSEA_API_V1 + "asset_contract"
18+
BUNDLES = OPENSEA_API_V1 + "bundles"
19+
EVENTS = OPENSEA_API_V1 + "events"
20+
COLLECTIONS = OPENSEA_API_V1 + "collections"
21+
LISTINGS = OPENSEA_LISTINGS_V1 + "orders"
22+
23+
24+
class AssetsOrderBy(ExtendedStrEnum):
25+
TOKEN_ID = "token_id"
26+
SALE_DATE = "sale_date"
27+
SALE_COUNT = "sale_count"
28+
VISITOR_COUNT = "visitor_count"
29+
SALE_PRICE = "sale_price"
30+
31+
32+
class Asset(ExtendedStrEnum):
33+
TOKEN_ID = "token_id"
34+
NUM_SALES = "num_sales"
35+
BACKGROUND_COLOR = "background_color"
36+
IMAGE_URL = "image_url"
37+
IMAGE_PREVIEW_URL = "image_preview_url"
38+
IMAGE_THUMBNAIL_URL = "image_thumbnail_url"
39+
IMAGE_ORIGINAL_URL = "image_original_url"
40+
ANIMATION_URL = "animation_url"
41+
ANIMATION_ORIGINAL_URL = "animation_original_url"
42+
NAME = "name"
43+
DESCRIPTION = "description"
44+
EXTERNAL_LINK = "external_link"
45+
ASSET_CONTRACT_DICT = "asset_contract"
46+
PERMALINK = "permalink"
47+
COLLECTION_DICT = "collection"
48+
DECIMALS = "decimals"
49+
TOKEN_METADATA = "token_metadata"
50+
OWNER_DICT = "owner"
51+
SELL_ORDERS = "sell_orders"
52+
CREATOR_DICT = "creator"
53+
TRAITS_DICT = "traits"
54+
LAST_SALE_DICT = "last_sale"
55+
TOP_BID = "top_bid"
56+
LISTING_DATE = "listing_date"
57+
IS_PRESALE = "is_presale"
58+
TRANSFER_FEE_PAYMENT_TOKEN = "transfer_fee_payment_token"
59+
TRANSFER_FEE = "transfer_fee"
60+
61+
62+
class AssetTraits(ExtendedStrEnum):
63+
TRAIT_TYPE = "trait_type"
64+
VALUE = "value"
65+
DISPLAY_TYPE = "display_type"
66+
67+
68+
class AssetContract(ExtendedStrEnum):
69+
ADDRESS = "address"
70+
NAME = "name"
71+
SYMBOL = "symbol"
72+
IMAGE_URL = "image_url"
73+
DESCRIPTION = "description"
74+
EXTERNAL_LINK = "external_link"
75+
76+
77+
class AssetOwner(ExtendedStrEnum):
78+
ADDRESS = 'address'
79+
CONFIG = 'config'
80+
PROFILE_IMG_URL = 'profile_img_url'
81+
USER = 'user'
82+
83+
84+
class AssetLastSale(ExtendedStrEnum):
85+
ASSET = 'asset'
86+
ASSET_BUNDLE = 'asset_bundle'
87+
EVENT_TYPE = 'event_type'
88+
EVENT_TIMESTAMP = 'event_timestamp'
89+
AUCTION_TYPE = 'auction_type'
90+
TOTAL_PRICE = 'total_price'
91+
PAYMENT_TOKEN_DICT = 'payment_token'
92+
TRANSACTION_DICT = 'transaction'
93+
CREATED_DATE = 'created_date'
94+
QUANTITY = 'quantity'

open_sea_v1/endpoints/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)