Skip to content

Commit a0877d0

Browse files
committed
README.md: Added some basic information about the package.
Added test runner, all tests pass. Minor fix: poorly coded paginator was tested and fixed. Minor fix: changed id attributes from responses from int to str.
1 parent 2e9733c commit a0877d0

File tree

8 files changed

+92
-42
lines changed

8 files changed

+92
-42
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
1-
# opensea-python-wrapper
2-
A convenient Python wrapper for the OpenSea API.
1+
# OpenSea Python API Wrapper
2+
A convenient package for interacting with the OpenSea API; which allows for retrieval of asset data from the OpenSea marketplace (https://opensea.io/).
3+
4+
# Warning about the dev branch
5+
* Do not expect the dev branch to be stable or complete.
6+
* There is no documentation yet. To get a sense on how the package works, have a look at the endpoint classes in the open_sea_v1 folder. You'll probably want to instanciate an endpoint class, and call it's methods.
7+
* There will be non backwards-compatible pushes and commit squashes on the dev branch.
8+
9+
# About the documentation
10+
* OpenSea API V1 Documentation: https://docs.opensea.io/reference/
11+
* Request an API key here: https://docs.opensea.io/reference/request-an-api-key
12+
* Anonymous API usage is limited to 2 queries per second.
13+

open_sea_v1/endpoints/client.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from abc import ABC
23
from dataclasses import dataclass
34
from typing import Optional, Generator
@@ -6,6 +7,8 @@
67

78
from open_sea_v1.responses.abc import BaseResponse
89

10+
logger = logging.getLogger(__name__)
11+
912
@dataclass
1013
class ClientParams:
1114
"""Common OpenSea Endpoint parameters to pass in."""
@@ -16,8 +19,24 @@ class ClientParams:
1619
api_key: Optional[str] = None
1720

1821
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.')
22+
self._validate_attrs()
23+
self._decrement_max_pages_attr()
24+
25+
def _validate_attrs(self) -> None:
26+
if self.limit is not None and not 0 < self.limit <= 300:
27+
raise ValueError(f'{self.limit=} must be over 0 and lesser or equal to 300.')
28+
if self.page_size is not None and not 0 <= self.page_size <= 50:
29+
raise ValueError(f'{self.page_size=} must be between 0 and 50.')
30+
if self.max_pages is not None and self.max_pages < 0:
31+
raise ValueError(f'{self.max_pages=} must be greater than or equal to 0.')
32+
33+
def _decrement_max_pages_attr(self) -> None:
34+
"""
35+
For OpenSea, the max pages attribute starts at zero.
36+
However, and for clarity our package, will have this value start at 1 and decrement it for OpenSea.
37+
"""
38+
if self.max_pages is not None:
39+
self.max_pages -= 1
2140

2241

2342
class BaseClient(ABC):
@@ -41,12 +60,12 @@ def _get_request(self, **kwargs) -> Response:
4160

4261
def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
4362
self.processed_pages = 0
44-
self.client_params.offset = 0
63+
self.client_params.offset = 0 if self.client_params.offset is None else self.client_params.offset
4564
self._http_response = None
4665

4766
while self.remaining_pages():
4867
self._http_response = self._get_request()
49-
if self.parsed_http_response: # edge case
68+
if self.parsed_http_response is not None: # edge case
5069
self.processed_pages += 1
5170
self.client_params.offset += self.client_params.page_size
5271
yield self.parsed_http_response

open_sea_v1/endpoints/tests/test_assets.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,3 @@ def test_param_order_by_sale_price(self):
7272
def test_param_order_by_visitor_count(self):
7373
pass # as far as I know this is not returned in the API http_response and done directly by OpenSea
7474

75-
def test_param_limit_cannot_be_below_0_or_above_50(self):
76-
self.assertRaises(ValueError, AssetsEndpoint, **{**self.default_asset_params, **{'client_params': ClientParams(limit="25")}})
77-
self.assertRaises(ValueError, AssetsEndpoint, **{**self.default_asset_params, **{'client_params': ClientParams(limit=-1)}})
78-
self.assertRaises(ValueError, AssetsEndpoint, **{**self.default_asset_params, **{'client_params': ClientParams(limit=51)}})

open_sea_v1/endpoints/tests/test_client.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44
from open_sea_v1.endpoints.client import ClientParams
55
from open_sea_v1.endpoints.events import EventsEndpoint, EventType
66

7+
class TestClientParams(TestCase):
8+
9+
def test_max_pages_attr_is_automatically_decremented_by_1(self):
10+
params = ClientParams(max_pages=1)
11+
self.assertEqual(params.max_pages, 0)
12+
13+
def test_max_pages_attr_raises_value_error_if_below_or_equal_to_zero(self):
14+
self.assertRaises(ValueError, ClientParams, max_pages=-1)
15+
16+
def test_limit_attr_raises_value_error_if_not_between_0_and_300(self):
17+
self.assertRaises(ValueError, ClientParams, limit=-1)
18+
self.assertRaises(ValueError, ClientParams, limit=301)
19+
20+
def test_page_size_attr_raises_value_error_if_not_between_0_and_50(self):
21+
self.assertRaises(ValueError, ClientParams, page_size=-1)
22+
self.assertRaises(ValueError, ClientParams, page_size=51)
23+
724

825
class TestBaseEndpointClient(TestCase):
926

@@ -17,8 +34,14 @@ def setUpClass(cls) -> None:
1734
token_id=str(10152),
1835
event_type=EventType.SUCCESSFUL,
1936
)
20-
cls.sample_client = EventsEndpoint(**cls.sample_client_kwargs)
21-
cls.sample_pages = list(cls.sample_client.get_pages())
37+
cls.sample_pages = list(cls.mk_events_endpoint().get_pages())
38+
39+
def setUp(self) -> None:
40+
self.sample_client = self.mk_events_endpoint()
41+
42+
@classmethod
43+
def mk_events_endpoint(cls) -> EventsEndpoint:
44+
return EventsEndpoint(**cls.sample_client_kwargs)
2245

2346
def test_remaining_pages_true_if_http_response_is_none(self):
2447
self.sample_client._http_response = None
@@ -30,13 +53,6 @@ def test_remaining_pages_does_not_raise_if_client_params_all_none(self):
3053
next(client.get_pages())
3154
client.remaining_pages() # assert not raises
3255

33-
def test_get_pages_resets_processed_pages_and_offset_attr_on_new_calls(self):
34-
for _ in range(2):
35-
next(self.sample_client.get_pages())
36-
self.assertEqual(self.sample_client.processed_pages, 1)
37-
expected_offset_value = self.sample_client.client_params.limit
38-
self.assertEqual(self.sample_client.client_params.offset, expected_offset_value)
39-
4056
def test_get_pages_does_not_append_empty_pages(self):
4157
no_empty_pages = all(not page == list() for page in self.sample_pages)
4258
self.assertTrue(no_empty_pages)
@@ -46,26 +62,22 @@ def test_get_pages_max_pages_and_limit_params_works(self):
4662
for page in self.sample_pages[:-1]:
4763
self.assertEqual(self.limit, len(page))
4864

49-
def test_pagination_works(self):
50-
id_list_1 = [[e.id for e in page] for page in self.sample_client.get_pages()]
51-
id_list_1 = list(chain.from_iterable(id_list_1))
52-
id_list_1.sort(reverse=True)
65+
def test_pagination_does_not_return_duplicates_between_different_pages(self):
5366

54-
self.sample_client.client_params = ClientParams(limit=4, offset=0, max_pages=2)
55-
id_list_2 = [[e.id for e in page] for page in self.sample_client.get_pages()]
56-
id_list_2 = list(chain.from_iterable(id_list_2))
57-
id_list_2.sort(reverse=True)
67+
def get_event_ids(offset) -> list[str]:
68+
self.sample_client.client_params = ClientParams(limit=5, offset=offset, max_pages=1)
69+
return [event.id for event in chain.from_iterable(self.sample_client.get_pages())]
5870

59-
self.assertEqual(len(id_list_2), 12) # updated limit * max_pages+1
60-
self.assertGreater(len(id_list_1), len(id_list_2))
61-
self.assertTrue(id_list_1[i] == id_list_2[i] for i in range(len(id_list_2)))
71+
pages = [get_event_ids(offset) for offset in range(0, 14, 5)]
72+
pages = list(chain.from_iterable(pages))
73+
total_events = len(pages)
74+
total_unique_events = len(set(pages))
75+
self.assertEqual(total_events, total_unique_events)
6276

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-
...
77+
def test_pagination_pages_are_in_perfect_sequence(self):
78+
"""Making sure we are not skipping things between pages by mistake."""
79+
self.sample_client.client_params = ClientParams(limit=2, offset=0, max_pages=1)
80+
short_page_event_ids = [e.id for e in list(self.sample_client.get_pages())[0]]
81+
self.sample_client.client_params = ClientParams(limit=3, offset=1, max_pages=1)
82+
longer_page_event_ids = [e.id for e in list(self.sample_client.get_pages())[0]]
83+
self.assertEqual(short_page_event_ids[-1], longer_page_event_ids[0])

open_sea_v1/responses/asset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def __post_init__(self):
8888
self._set_optional_attrs()
8989

9090
def _set_common_attrs(self):
91-
self.token_id = self._json["token_id"]
91+
self.token_id = str(self._json["token_id"])
9292
self.num_sales = self._json["num_sales"]
9393
self.background_color = self._json["background_color"]
9494
self.image_url = self._json["image_url"]
@@ -103,7 +103,7 @@ def _set_common_attrs(self):
103103
self.permalink = self._json["permalink"]
104104
self.decimals = self._json["decimals"]
105105
self.token_metadata = self._json["token_metadata"]
106-
self.id = self._json["id"]
106+
self.id = str(self._json["id"])
107107

108108
def _set_optional_attrs(self):
109109
"""

open_sea_v1/responses/event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __post_init__(self):
3636
self.ending_price = self._json['ending_price']
3737
self.event_type = self._json['event_type']
3838
self.from_account = self._json['from_account']
39-
self.id = self._json['id']
39+
self.id = str(self._json['id'])
4040
self.owner_account = self._json['owner_account']
4141
self.quantity = self._json['quantity']
4242
self.starting_price = self._json['starting_price']

open_sea_v1/tests/__init__.py

Whitespace-only changes.

open_sea_v1/tests/run_tests.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from unittest import TestLoader, TextTestRunner
2+
3+
4+
def discover_and_run_tests() -> None:
5+
loader = TestLoader()
6+
tests = loader.discover('..', pattern='test_*')
7+
test_runner = TextTestRunner()
8+
test_runner.run(tests)
9+
10+
11+
if __name__ == '__main__':
12+
discover_and_run_tests()

0 commit comments

Comments
 (0)