Skip to content

Commit 30525c7

Browse files
committed
Project structure changes, and added tests to endpoint paginator method.
1 parent 34c0513 commit 30525c7

20 files changed

+161
-125
lines changed

open_sea_v1/endpoints/__init__.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +0,0 @@
1-
"""
2-
Exposes classes and objects meant to be used by You!
3-
Import other modules at your own risk, as their location may change.
4-
"""
5-
from open_sea_v1.endpoints.endpoint_client import _ClientParams as ClientParams
6-
7-
from open_sea_v1.endpoints.endpoint_assets import _AssetsEndpoint as AssetsEndpoint
8-
from open_sea_v1.endpoints.endpoint_assets import _AssetsOrderBy as AssetsOrderBy
9-
10-
from open_sea_v1.endpoints.endpoint_events import _EventsEndpoint as EventsEndpoint
11-
from open_sea_v1.endpoints.endpoint_events import AuctionType
12-
from open_sea_v1.endpoints.endpoint_events import EventType
13-
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
from requests import Response
55

6-
from open_sea_v1.endpoints.endpoint_client import _ClientParams
7-
from open_sea_v1.responses.response_abc import _OpenSeaResponse
6+
from open_sea_v1.endpoints.client import ClientParams
7+
from open_sea_v1.responses.abc import BaseResponse
88

99

10-
class BaseOpenSeaEndpoint(ABC):
10+
class BaseEndpoint(ABC):
1111

1212
@property
1313
@abstractmethod
@@ -16,7 +16,7 @@ def __post_init__(self):
1616

1717
@property
1818
@abstractmethod
19-
def client_params(self) -> _ClientParams:
19+
def client_params(self) -> ClientParams:
2020
"""Instance of common OpenSea Endpoint parameters."""
2121

2222
@property
@@ -26,12 +26,7 @@ def url(self) -> str:
2626

2727
@property
2828
@abstractmethod
29-
def get_pages(self) -> Generator[list[list[_OpenSeaResponse]], None, None]:
30-
"""Returns all pages for the query."""
31-
32-
@property
33-
@abstractmethod
34-
def parsed_http_response(self) -> Union[list[_OpenSeaResponse], _OpenSeaResponse]:
29+
def parsed_http_response(self) -> Union[list[BaseResponse], BaseResponse]:
3530
"""Parsed JSON dictionnary from HTTP Response."""
3631

3732
@abstractmethod
@@ -42,3 +37,7 @@ def _get_request(self) -> Response:
4237
@abstractmethod
4338
def _validate_request_params(self) -> None:
4439
""""""
40+
41+
@abstractmethod
42+
def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
43+
"""Returns all pages for the query."""
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from enum import Enum
33
from typing import Optional, Generator
44

5-
from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint
6-
from open_sea_v1.endpoints.endpoint_client import BaseOpenSeaClient, _ClientParams
7-
from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints
8-
from open_sea_v1.responses import AssetResponse
5+
from open_sea_v1.endpoints.abc import BaseEndpoint
6+
from open_sea_v1.endpoints.client import BaseClient, ClientParams
7+
from open_sea_v1.endpoints.urls import EndpointURLS
8+
from open_sea_v1.responses.asset import AssetResponse
99

1010

11-
class _AssetsOrderBy(str, Enum):
11+
class AssetsOrderBy(str, Enum):
1212
"""
1313
Helper Enum for remembering the possible values for the order_by param of the AssetsEndpoint class.
1414
"""
@@ -20,7 +20,7 @@ class _AssetsOrderBy(str, Enum):
2020

2121

2222
@dataclass
23-
class _AssetsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
23+
class AssetsEndpoint(BaseClient, BaseEndpoint):
2424
"""
2525
Opensea API Assets Endpoint
2626
@@ -52,21 +52,21 @@ class _AssetsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
5252
5353
:return: Parsed JSON
5454
"""
55-
client_params: _ClientParams = None
55+
client_params: ClientParams = None
5656
asset_contract_address: Optional[list[str]] = None
5757
asset_contract_addresses: Optional[str] = None
5858
token_ids: Optional[list[int]] = None
5959
collection: Optional[str] = None
6060
owner: Optional[str] = None
61-
order_by: Optional[_AssetsOrderBy] = None
61+
order_by: Optional[AssetsOrderBy] = None
6262
order_direction: str = None
6363

6464
def __post_init__(self):
6565
self._validate_request_params()
6666

6767
@property
6868
def url(self):
69-
return OpenseaApiEndpoints.ASSETS.value
69+
return EndpointURLS.ASSETS.value
7070

7171
@property
7272
def parsed_http_response(self) -> list[AssetResponse]:
@@ -129,10 +129,10 @@ def _validate_order_by(self) -> None:
129129
if self.order_by is None:
130130
return
131131

132-
if self.order_by not in (_AssetsOrderBy.TOKEN_ID, _AssetsOrderBy.SALE_COUNT, _AssetsOrderBy.SALE_DATE, _AssetsOrderBy.SALE_PRICE, _AssetsOrderBy.VISITOR_COUNT):
132+
if self.order_by not in (AssetsOrderBy.TOKEN_ID, AssetsOrderBy.SALE_COUNT, AssetsOrderBy.SALE_DATE, AssetsOrderBy.SALE_PRICE, AssetsOrderBy.VISITOR_COUNT):
133133
raise ValueError(
134134
f"order_by param value ({self.order_by}) is invalid. "
135-
f"Must be a value from {_AssetsOrderBy.list()}, case sensitive."
135+
f"Must be a value from {AssetsOrderBy.list()}, case sensitive."
136136
)
137137

138138
def _validate_limit(self):
Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44

55
from requests import Response, request
66

7-
from open_sea_v1.responses import OpenSeaAPIResponse
7+
from open_sea_v1.responses.abc import BaseResponse
88

99
@dataclass
10-
class _ClientParams:
10+
class ClientParams:
1111
"""Common OpenSea Endpoint parameters to pass in."""
1212
offset: int = 0
1313
limit: int = 20
1414
max_pages: Optional[int] = None
1515
api_key: Optional[str] = None
1616

1717

18-
class BaseOpenSeaClient(ABC):
19-
client_params: _ClientParams
18+
class BaseClient(ABC):
19+
client_params: ClientParams
2020
processed_pages: int = 0
2121
response = None
2222
parsed_http_response = None
@@ -34,19 +34,29 @@ def _get_request(self, **kwargs) -> Response:
3434
updated_kwargs = kwargs | self.http_headers
3535
return request('GET', self.url, **updated_kwargs)
3636

37-
def get_pages(self) -> Generator[list[list[OpenSeaAPIResponse]], None, None]:
37+
def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
38+
self.processed_pages = 0
39+
self.client_params.offset = 0
3840
self._http_response = None
41+
3942
while self.remaining_pages():
4043
self._http_response = self._get_request()
41-
yield self.parsed_http_response
42-
self.client_params.offset += self.client_params.limit
43-
self.processed_pages += 1
44+
if self.parsed_http_response: # edge case
45+
self.processed_pages += 1
46+
self.client_params.offset += self.client_params.limit
47+
yield self.parsed_http_response
4448

4549
def remaining_pages(self) -> bool:
4650
if self._http_response is None:
4751
return True
48-
if self.client_params.max_pages is not None and self.processed_pages <= self.client_params.max_pages:
52+
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+
)):
4958
return True
50-
if len(self.response) >= self.client_params.offset:
59+
60+
if is_not_the_last_page := len(self.parsed_http_response) >= self.client_params.offset:
5161
return True
5262
return False
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from dataclasses import dataclass
22
from datetime import datetime
3-
from typing import Optional, Generator
3+
from typing import Optional
44

55
from requests import Response
66

7-
from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint
8-
from open_sea_v1.endpoints.endpoint_client import BaseOpenSeaClient, _ClientParams
9-
from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints
7+
from open_sea_v1.endpoints.client import BaseClient, ClientParams
8+
from open_sea_v1.endpoints.abc import BaseEndpoint
9+
from open_sea_v1.endpoints.urls import EndpointURLS
1010
from open_sea_v1.helpers.extended_classes import ExtendedStrEnum
11-
from open_sea_v1.responses import EventResponse
12-
from open_sea_v1.responses.response_abc import _OpenSeaResponse
11+
from open_sea_v1.responses.event import EventResponse
1312

1413

1514
class EventType(ExtendedStrEnum):
@@ -35,7 +34,7 @@ class AuctionType(ExtendedStrEnum):
3534

3635

3736
@dataclass
38-
class _EventsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
37+
class EventsEndpoint(BaseClient, BaseEndpoint):
3938
"""
4039
Opensea API Events Endpoint
4140
@@ -67,7 +66,7 @@ class _EventsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
6766
6867
:return: Parsed JSON
6968
"""
70-
client_params: _ClientParams = None
69+
client_params: ClientParams = None
7170
asset_contract_address: str = None
7271
token_id: Optional[str] = None
7372
collection_slug: Optional[str] = None
@@ -83,7 +82,7 @@ def __post_init__(self):
8382

8483
@property
8584
def url(self) -> str:
86-
return OpenseaApiEndpoints.EVENTS.value
85+
return EndpointURLS.EVENTS.value
8786

8887
def _get_request(self, **kwargs) -> Response:
8988
params = dict(
@@ -115,6 +114,9 @@ def _validate_request_params(self) -> None:
115114
self._validate_params_occurred_before_and_occurred_after()
116115

117116
def _validate_param_event_type(self) -> None:
117+
if self.event_type is None:
118+
return
119+
118120
if not isinstance(self.event_type, (str, EventType)):
119121
raise TypeError('Invalid event_type type. Must be str or EventType Enum.', f"{self.event_type=}")
120122

open_sea_v1/endpoints/tests/test_assets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from itertools import combinations
22
from unittest import TestCase
33

4-
from open_sea_v1.endpoints import AssetsEndpoint, AssetsOrderBy, ClientParams
5-
from open_sea_v1.responses import AssetResponse
4+
from open_sea_v1.endpoints.assets import AssetsEndpoint, AssetsOrderBy, ClientParams
5+
from open_sea_v1.responses.asset import AssetResponse
66

77

88
class TestAssetsRequest(TestCase):
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from itertools import chain
2+
from unittest import TestCase
3+
4+
from open_sea_v1.endpoints.client import ClientParams
5+
from open_sea_v1.endpoints.events import EventsEndpoint, EventType
6+
7+
8+
class TestBaseEndpointClient(TestCase):
9+
10+
@classmethod
11+
def setUpClass(cls) -> None:
12+
cls.max_pages = 2
13+
cls.limit = 5
14+
cls.sample_client = EventsEndpoint(
15+
client_params=ClientParams(max_pages=cls.max_pages, limit=cls.limit),
16+
asset_contract_address="0x76be3b62873462d2142405439777e971754e8e77",
17+
token_id=str(10152),
18+
event_type=EventType.SUCCESSFUL,
19+
)
20+
cls.sample_pages = list(cls.sample_client.get_pages())
21+
22+
def test_remaining_pages_true_if_http_response_is_none(self):
23+
self.sample_client._http_response = None
24+
self.assertTrue(self.sample_client.remaining_pages())
25+
26+
def test_get_pages_resets_processed_pages_and_offset_attr_on_new_calls(self):
27+
for _ in range(2):
28+
next(self.sample_client.get_pages())
29+
self.assertEqual(self.sample_client.processed_pages, 1)
30+
expected_offset_value = self.sample_client.client_params.limit
31+
self.assertEqual(self.sample_client.client_params.offset, expected_offset_value)
32+
33+
def test_get_pages_does_not_append_empty_pages(self):
34+
no_empty_pages = all(not page == list() for page in self.sample_pages)
35+
self.assertTrue(no_empty_pages)
36+
37+
def test_get_pages_max_pages_and_limit_params_works(self):
38+
self.assertLessEqual(len(self.sample_pages), self.max_pages + 1)
39+
for page in self.sample_pages[:-1]:
40+
self.assertEqual(self.limit, len(page))
41+
42+
def test_pagination_works(self):
43+
id_list_1 = [[e.id for e in page] for page in self.sample_client.get_pages()]
44+
id_list_1 = list(chain.from_iterable(id_list_1))
45+
id_list_1.sort(reverse=True)
46+
47+
self.sample_client.client_params = ClientParams(limit=4, offset=0, max_pages=2)
48+
id_list_2 = [[e.id for e in page] for page in self.sample_client.get_pages()]
49+
id_list_2 = list(chain.from_iterable(id_list_2))
50+
id_list_2.sort(reverse=True)
51+
52+
self.assertEqual(len(id_list_2), 12) # updated limit * max_pages+1
53+
self.assertGreater(len(id_list_1), len(id_list_2))
54+
self.assertTrue(id_list_1[i] == id_list_2[i] for i in range(len(id_list_2)))

open_sea_v1/endpoints/tests/test_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from unittest import TestCase
22
from datetime import datetime, timedelta
33

4-
from open_sea_v1.endpoints import EventsEndpoint, EventType, AuctionType
5-
from open_sea_v1.endpoints import ClientParams
4+
from open_sea_v1.endpoints.events import EventsEndpoint, EventType, AuctionType
5+
from open_sea_v1.endpoints.client import ClientParams
66

77

88
class TestEventsEndpoint(TestCase):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
OPENSEA_LISTINGS_V1 = "https://api.opensea.io/wyvern/v1/"
55

66

7-
class OpenseaApiEndpoints(str, Enum):
7+
class EndpointURLS(str, Enum):
88
ASSET = OPENSEA_API_V1 + "asset"
99
ASSETS = OPENSEA_API_V1 + "assets"
1010
ASSET_CONTRACT = OPENSEA_API_V1 + "asset_contract"

open_sea_v1/helpers/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +0,0 @@
1-
"""
2-
Exposes classes and objects meant to be used by You!
3-
Import other modules at your own risk, as their location may change.
4-
"""
5-
from open_sea_v1.helpers.ether_converter import EtherConverter, EtherUnit
6-
from open_sea_v1.helpers.extended_classes import ExtendedStrEnum

0 commit comments

Comments
 (0)