Skip to content

Commit b7fa57e

Browse files
committed
Added various validation checks.
Fixed issue with remaining pages func. Adjusted load methods on response parser class. Added __str__ methods on multiple Response classes. Fixed Traits/Last Sale in AssetResponse class .
1 parent 30525c7 commit b7fa57e

File tree

11 files changed

+96
-28
lines changed

11 files changed

+96
-28
lines changed

open_sea_v1/endpoints/assets.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class AssetsEndpoint(BaseClient, BaseEndpoint):
5555
client_params: ClientParams = None
5656
asset_contract_address: Optional[list[str]] = None
5757
asset_contract_addresses: Optional[str] = None
58-
token_ids: Optional[list[int]] = None
58+
token_ids: Optional[list[str]] = None
5959
collection: Optional[str] = None
6060
owner: Optional[str] = None
6161
order_by: Optional[AssetsOrderBy] = None
@@ -136,5 +136,9 @@ def _validate_order_by(self) -> None:
136136
)
137137

138138
def _validate_limit(self):
139-
if not isinstance(self.client_params.limit, int) or not 0 <= self.client_params.limit <= 50:
139+
if self.client_params.limit is None:
140+
return
141+
if not isinstance(self.client_params.limit, int):
142+
raise TypeError("limit client param must be an int")
143+
if not 0 <= self.client_params.limit <= 50:
140144
raise ValueError(f"limit param must be an int between 0 and 50.")

open_sea_v1/endpoints/client.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
class ClientParams:
1111
"""Common OpenSea Endpoint parameters to pass in."""
1212
offset: int = 0
13-
limit: int = 20
13+
page_size: int = 50
14+
limit: Optional[int] = None
1415
max_pages: Optional[int] = None
1516
api_key: Optional[str] = None
1617

18+
def __post_init__(self):
19+
if self.limit is not None and not 0 < int(self.limit) <= 300:
20+
raise ValueError(f'{self.limit=} max value is 300.')
21+
1722

1823
class BaseClient(ABC):
1924
client_params: ClientParams
@@ -43,18 +48,16 @@ def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
4348
self._http_response = self._get_request()
4449
if self.parsed_http_response: # edge case
4550
self.processed_pages += 1
46-
self.client_params.offset += self.client_params.limit
51+
self.client_params.offset += self.client_params.page_size
4752
yield self.parsed_http_response
4853

4954
def remaining_pages(self) -> bool:
5055
if self._http_response is None:
5156
return True
5257

53-
if all((
54-
(max_pages_was_set := self.client_params.max_pages is not None),
55-
(previous_page_was_not_empty := len(self.parsed_http_response) > 0),
56-
(remaining_pages_until_max_pages := self.processed_pages <= self.client_params.max_pages),
57-
)):
58+
if (max_pages_was_set := self.client_params.max_pages is not None) and \
59+
(previous_page_was_not_empty := len(self.parsed_http_response) > 0) and \
60+
(remaining_pages_until_max_pages := self.processed_pages <= self.client_params.max_pages):
5861
return True
5962

6063
if is_not_the_last_page := len(self.parsed_http_response) >= self.client_params.offset:

open_sea_v1/endpoints/events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class EventsEndpoint(BaseClient, BaseEndpoint):
6868
"""
6969
client_params: ClientParams = None
7070
asset_contract_address: str = None
71-
token_id: Optional[str] = None
71+
token_id: Optional[int] = None
7272
collection_slug: Optional[str] = None
7373
account_address: Optional[str] = None
7474
occurred_before: Optional[datetime] = None
@@ -96,7 +96,7 @@ def _get_request(self, **kwargs) -> Response:
9696
account_address=self.account_address,
9797
auction_type=self.auction_type,
9898
occurred_before=self.occurred_before,
99-
occurred_after=self.occurred_after
99+
occurred_after=self.occurred_after,
100100
)
101101
get_request_kwargs = dict(params=params)
102102
self._http_response = super()._get_request(**get_request_kwargs)

open_sea_v1/endpoints/tests/test_client.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,25 @@ class TestBaseEndpointClient(TestCase):
1111
def setUpClass(cls) -> None:
1212
cls.max_pages = 2
1313
cls.limit = 5
14-
cls.sample_client = EventsEndpoint(
14+
cls.sample_client_kwargs = dict(
1515
client_params=ClientParams(max_pages=cls.max_pages, limit=cls.limit),
1616
asset_contract_address="0x76be3b62873462d2142405439777e971754e8e77",
1717
token_id=str(10152),
1818
event_type=EventType.SUCCESSFUL,
1919
)
20+
cls.sample_client = EventsEndpoint(**cls.sample_client_kwargs)
2021
cls.sample_pages = list(cls.sample_client.get_pages())
2122

2223
def test_remaining_pages_true_if_http_response_is_none(self):
2324
self.sample_client._http_response = None
2425
self.assertTrue(self.sample_client.remaining_pages())
2526

27+
def test_remaining_pages_does_not_raise_if_client_params_all_none(self):
28+
updated_kwargs = self.sample_client_kwargs | dict(client_params=ClientParams(max_pages=None, api_key=None))
29+
client = EventsEndpoint(**updated_kwargs)
30+
next(client.get_pages())
31+
client.remaining_pages() # assert not raises
32+
2633
def test_get_pages_resets_processed_pages_and_offset_attr_on_new_calls(self):
2734
for _ in range(2):
2835
next(self.sample_client.get_pages())
@@ -52,3 +59,13 @@ def test_pagination_works(self):
5259
self.assertEqual(len(id_list_2), 12) # updated limit * max_pages+1
5360
self.assertGreater(len(id_list_1), len(id_list_2))
5461
self.assertTrue(id_list_1[i] == id_list_2[i] for i in range(len(id_list_2)))
62+
63+
def test_pagination_does_not_return_duplicates_between_different_pages(self):
64+
raise NotImplementedError
65+
self.sample_client.client_params = ClientParams(limit=5, offset=0, max_pages=1)
66+
page_1_event_ids = [e.event_id for e in self.sample_client.get_pages()]
67+
self.sample_client.client_params = ClientParams(limit=5, offset=5, max_pages=1)
68+
page_2_event_ids = self.sample_client.get_pages()
69+
self.sample_client.client_params = ClientParams(limit=5, offset=10, max_pages=1)
70+
page_3_event_ids = self.sample_client.get_pages()
71+
...

open_sea_v1/helpers/response_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ def dump(self, to_parse: Optional[Union[OpenSeaResponse, list[OpenSeaResponse]]]
2626
with open(str(self.destination), 'w') as f:
2727
json.dump(the_jsons, f)
2828

29-
def load(self) -> Any:
29+
def load(self, json_path: Optional[Path] = None) -> Any:
30+
json_path = self.destination if not json_path else json_path
3031
with open(str(self.destination), 'r') as f:
3132
parsed_json = json.load(f)
3233
return [self.response_type(collection) for collection in parsed_json]
34+
35+
def load_from_dir(self) -> Any:
36+
detected_json_files = (p for p in self.destination.iterdir() if '.json' in p.name and not p.is_dir())
37+
resp = list()
38+
for json_path in detected_json_files:
39+
resp.append(self.load(destination=json_path))

open_sea_v1/responses/asset.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Assigns attributes to dictionnary values for easier object navigation.
33
"""
44
from dataclasses import dataclass
5+
from typing import Optional
56

67
from open_sea_v1.responses.abc import BaseResponse
78
from open_sea_v1.responses.collection import CollectionResponse
@@ -77,8 +78,10 @@ def __post_init__(self):
7778
class AssetResponse(BaseResponse):
7879
_json: dict
7980

80-
def __str__(self) -> str:
81-
return f"({AssetResponse.__name__}, id={self.token_id.zfill(5)}, name={self.name})"
81+
def __str__(self):
82+
id_str = f"token_id={self.token_id.zfill(5)}"
83+
name = f"name={self.name}"
84+
return " ".join([id_str, name])
8285

8386
def __post_init__(self):
8487
self._set_common_attrs()
@@ -123,17 +126,23 @@ def owner(self) -> _Owner:
123126
return _Owner(self._json['owner'])
124127

125128
@property
126-
def traits(self) -> list[_Traits]:
127-
return [_Traits(traits) for traits in self._json['traits']]
129+
def traits(self) -> Optional[list[_Traits]]:
130+
traits = self._json.get('traits')
131+
if traits:
132+
return [_Traits(traits) for traits in self._json['traits']]
133+
return None
128134

129135
@property
130-
def last_sale(self) -> _LastSale:
131-
return _LastSale(self._json['last_sale'])
136+
def last_sale(self) -> Optional[_LastSale]:
137+
last_sale = self._json.get('last_sale')
138+
if last_sale:
139+
return _LastSale(self._json['last_sale'])
140+
return None
132141

133142
@property
134143
def collection(self):
135144
return CollectionResponse(self._json['collection'])
136145

137146
@property
138-
def creator(self) -> dict:
139-
return self._json['creator']
147+
def creator(self) -> Optional[dict]:
148+
return self._json.get('creator')

open_sea_v1/responses/collection.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Assigns attributes to dictionnary values for easier object navigation.
33
"""
44
from dataclasses import dataclass
5+
from typing import Optional
56

67
from open_sea_v1.responses.abc import BaseResponse
78

@@ -11,7 +12,7 @@ class _CollectionStats:
1112
_json: dict
1213

1314
def __str__(self) -> str:
14-
return f"({_CollectionStats.__name__}, {self.floor_price=}, {self.average_price=}, {self.market_cap=})"
15+
return f"{self.floor_price=} {self.average_price=} {self.market_cap=})"
1516

1617
def __post_init__(self):
1718
self.one_day_volume = self._json["one_day_volume"]
@@ -42,9 +43,11 @@ class CollectionResponse(BaseResponse):
4243
_json: dict
4344

4445
def __str__(self) -> str:
45-
return f"({CollectionResponse.__name__}, {self.name=}, {self.short_description=})"
46+
return f"{self.name=} {self.short_description=})"
4647

4748
def __post_init__(self):
49+
self.primary_asset_contracts: Optional[list] = self._json.get('primary_asset_contracts')
50+
self.traits: Optional[dict] = self._json.get('traits')
4851
self.banner_image_url = self._json["banner_image_url"]
4952
self.chat_url = self._json["chat_url"]
5053
self.created_date = self._json["created_date"]
@@ -77,5 +80,6 @@ def __post_init__(self):
7780
self.name = self._json["name"]
7881

7982
@property
80-
def stats(self) -> _CollectionStats:
81-
return _CollectionStats(self._json['stats'])
83+
def stats(self) -> Optional[_CollectionStats]:
84+
stats = self._json.get('stats')
85+
return _CollectionStats(stats) if stats else None

open_sea_v1/responses/event.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
from dataclasses import dataclass
22

3+
from open_sea_v1.helpers.ether_converter import EtherConverter, EtherUnit
34
from open_sea_v1.responses.asset import AssetResponse
45
from open_sea_v1.responses.abc import BaseResponse
56

6-
7+
import locale
78
@dataclass
89
class EventResponse(BaseResponse):
910
_json: dict
1011

1112
def __str__(self) -> str:
12-
return f"{self.asset.name[:20]}, {self.transaction['timestamp'][:10]}, {self.quantity=}, {self.total_price=}"
13+
locale.setlocale(locale.LC_ALL, '') # big number str formater
14+
15+
name = self.asset.name[:20]
16+
transaction_date = f"{self.transaction['timestamp'][:10]} {self.transaction['timestamp'][11:-3]}"
17+
usd_price = round(self.usd_price / int(self.quantity), 2)
18+
usd_price = f"{usd_price:,.2f}"
19+
usd_price = f"{usd_price} USD"
20+
eth_price = self.eth_price / int(self.quantity)
21+
eth_price = f"{eth_price:.4f} ETH" # trailing zeros
22+
23+
str_representation =" ".join([name, transaction_date, usd_price, eth_price])
24+
return str_representation
1325

1426
def __post_init__(self):
1527
self.approved_account = self._json['approved_account']
1628
self.asset_bundle = self._json['asset_bundle']
1729
self.auction_type = self._json['auction_type']
18-
# self.big_amount = self._json['big_amount']
1930
self.collection_slug = self._json['collection_slug']
2031
self.contract_address = self._json['contract_address']
2132
self.created_date = self._json['created_date']
@@ -33,6 +44,17 @@ def __post_init__(self):
3344
self.total_price = self._json['total_price']
3445
self.bid_amount = self._json['bid_amount']
3546

47+
@property
48+
def eth_price(self):
49+
eth_price = EtherConverter(quantity=self.total_price, unit=EtherUnit.WEI).ether
50+
return eth_price
51+
52+
@property
53+
def usd_price(self):
54+
eth_to_usd_price = float(self.payment_token['usd_price']) # 'eth_price' key also available
55+
usd_price = round(self.eth_price * eth_to_usd_price, 2)
56+
return usd_price
57+
3658
@property
3759
def asset(self) -> AssetResponse:
3860
return AssetResponse(self._json['asset'])

open_sea_v1/responses/tests/test_reponse_event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class TestEventsObj(ResponseTestHelper):
1414
def setUpClass(cls) -> None:
1515
cls.events = cls.create_and_get(EventsEndpoint, **cls.events_default_kwargs)
1616
cls.event = cls.events[0]
17+
event_name_str = str(cls.event)
1718

1819
def test_attributes_do_not_raise_unexpected_exceptions(self):
1920
self.assert_attributes_do_not_raise_unexpected_exceptions(self.event)

open_sea_v1/responses/tests/test_response_asset.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def setUpClass(cls) -> None:
1515
params = cls.default_asset_params | dict(token_ids=[1, 14, 33])
1616
cls.assets = cls.create_and_get(AssetsEndpoint, **params)
1717
cls.asset = cls.assets[0]
18+
...
1819

1920
def test_attributes_do_not_raise_unexpected_exceptions(self):
2021
self.assert_attributes_do_not_raise_unexpected_exceptions(self.asset)

0 commit comments

Comments
 (0)