Skip to content

Commit

Permalink
Project structure changes, and added tests to endpoint paginator method.
Browse files Browse the repository at this point in the history
  • Loading branch information
dehidehidehi committed Aug 5, 2021
1 parent 34c0513 commit 30525c7
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 125 deletions.
13 changes: 0 additions & 13 deletions open_sea_v1/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +0,0 @@
"""
Exposes classes and objects meant to be used by You!
Import other modules at your own risk, as their location may change.
"""
from open_sea_v1.endpoints.endpoint_client import _ClientParams as ClientParams

from open_sea_v1.endpoints.endpoint_assets import _AssetsEndpoint as AssetsEndpoint
from open_sea_v1.endpoints.endpoint_assets import _AssetsOrderBy as AssetsOrderBy

from open_sea_v1.endpoints.endpoint_events import _EventsEndpoint as EventsEndpoint
from open_sea_v1.endpoints.endpoint_events import AuctionType
from open_sea_v1.endpoints.endpoint_events import EventType

Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

from requests import Response

from open_sea_v1.endpoints.endpoint_client import _ClientParams
from open_sea_v1.responses.response_abc import _OpenSeaResponse
from open_sea_v1.endpoints.client import ClientParams
from open_sea_v1.responses.abc import BaseResponse


class BaseOpenSeaEndpoint(ABC):
class BaseEndpoint(ABC):

@property
@abstractmethod
Expand All @@ -16,7 +16,7 @@ def __post_init__(self):

@property
@abstractmethod
def client_params(self) -> _ClientParams:
def client_params(self) -> ClientParams:
"""Instance of common OpenSea Endpoint parameters."""

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

@property
@abstractmethod
def get_pages(self) -> Generator[list[list[_OpenSeaResponse]], None, None]:
"""Returns all pages for the query."""

@property
@abstractmethod
def parsed_http_response(self) -> Union[list[_OpenSeaResponse], _OpenSeaResponse]:
def parsed_http_response(self) -> Union[list[BaseResponse], BaseResponse]:
"""Parsed JSON dictionnary from HTTP Response."""

@abstractmethod
Expand All @@ -42,3 +37,7 @@ def _get_request(self) -> Response:
@abstractmethod
def _validate_request_params(self) -> None:
""""""

@abstractmethod
def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
"""Returns all pages for the query."""
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from enum import Enum
from typing import Optional, Generator

from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint
from open_sea_v1.endpoints.endpoint_client import BaseOpenSeaClient, _ClientParams
from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints
from open_sea_v1.responses import AssetResponse
from open_sea_v1.endpoints.abc import BaseEndpoint
from open_sea_v1.endpoints.client import BaseClient, ClientParams
from open_sea_v1.endpoints.urls import EndpointURLS
from open_sea_v1.responses.asset import AssetResponse


class _AssetsOrderBy(str, Enum):
class AssetsOrderBy(str, Enum):
"""
Helper Enum for remembering the possible values for the order_by param of the AssetsEndpoint class.
"""
Expand All @@ -20,7 +20,7 @@ class _AssetsOrderBy(str, Enum):


@dataclass
class _AssetsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
class AssetsEndpoint(BaseClient, BaseEndpoint):
"""
Opensea API Assets Endpoint
Expand Down Expand Up @@ -52,21 +52,21 @@ class _AssetsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
:return: Parsed JSON
"""
client_params: _ClientParams = None
client_params: ClientParams = None
asset_contract_address: Optional[list[str]] = None
asset_contract_addresses: Optional[str] = None
token_ids: Optional[list[int]] = None
collection: Optional[str] = None
owner: Optional[str] = None
order_by: Optional[_AssetsOrderBy] = None
order_by: Optional[AssetsOrderBy] = None
order_direction: str = None

def __post_init__(self):
self._validate_request_params()

@property
def url(self):
return OpenseaApiEndpoints.ASSETS.value
return EndpointURLS.ASSETS.value

@property
def parsed_http_response(self) -> list[AssetResponse]:
Expand Down Expand Up @@ -129,10 +129,10 @@ def _validate_order_by(self) -> None:
if self.order_by is None:
return

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

def _validate_limit(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@

from requests import Response, request

from open_sea_v1.responses import OpenSeaAPIResponse
from open_sea_v1.responses.abc import BaseResponse

@dataclass
class _ClientParams:
class ClientParams:
"""Common OpenSea Endpoint parameters to pass in."""
offset: int = 0
limit: int = 20
max_pages: Optional[int] = None
api_key: Optional[str] = None


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

def get_pages(self) -> Generator[list[list[OpenSeaAPIResponse]], None, None]:
def get_pages(self) -> Generator[list[list[BaseResponse]], None, None]:
self.processed_pages = 0
self.client_params.offset = 0
self._http_response = None

while self.remaining_pages():
self._http_response = self._get_request()
yield self.parsed_http_response
self.client_params.offset += self.client_params.limit
self.processed_pages += 1
if self.parsed_http_response: # edge case
self.processed_pages += 1
self.client_params.offset += self.client_params.limit
yield self.parsed_http_response

def remaining_pages(self) -> bool:
if self._http_response is None:
return True
if self.client_params.max_pages is not None and self.processed_pages <= self.client_params.max_pages:

if all((
(max_pages_was_set := self.client_params.max_pages is not None),
(previous_page_was_not_empty := len(self.parsed_http_response) > 0),
(remaining_pages_until_max_pages := self.processed_pages <= self.client_params.max_pages),
)):
return True
if len(self.response) >= self.client_params.offset:

if is_not_the_last_page := len(self.parsed_http_response) >= self.client_params.offset:
return True
return False
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Generator
from typing import Optional

from requests import Response

from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint
from open_sea_v1.endpoints.endpoint_client import BaseOpenSeaClient, _ClientParams
from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints
from open_sea_v1.endpoints.client import BaseClient, ClientParams
from open_sea_v1.endpoints.abc import BaseEndpoint
from open_sea_v1.endpoints.urls import EndpointURLS
from open_sea_v1.helpers.extended_classes import ExtendedStrEnum
from open_sea_v1.responses import EventResponse
from open_sea_v1.responses.response_abc import _OpenSeaResponse
from open_sea_v1.responses.event import EventResponse


class EventType(ExtendedStrEnum):
Expand All @@ -35,7 +34,7 @@ class AuctionType(ExtendedStrEnum):


@dataclass
class _EventsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
class EventsEndpoint(BaseClient, BaseEndpoint):
"""
Opensea API Events Endpoint
Expand Down Expand Up @@ -67,7 +66,7 @@ class _EventsEndpoint(BaseOpenSeaClient, BaseOpenSeaEndpoint):
:return: Parsed JSON
"""
client_params: _ClientParams = None
client_params: ClientParams = None
asset_contract_address: str = None
token_id: Optional[str] = None
collection_slug: Optional[str] = None
Expand All @@ -83,7 +82,7 @@ def __post_init__(self):

@property
def url(self) -> str:
return OpenseaApiEndpoints.EVENTS.value
return EndpointURLS.EVENTS.value

def _get_request(self, **kwargs) -> Response:
params = dict(
Expand Down Expand Up @@ -115,6 +114,9 @@ def _validate_request_params(self) -> None:
self._validate_params_occurred_before_and_occurred_after()

def _validate_param_event_type(self) -> None:
if self.event_type is None:
return

if not isinstance(self.event_type, (str, EventType)):
raise TypeError('Invalid event_type type. Must be str or EventType Enum.', f"{self.event_type=}")

Expand Down
4 changes: 2 additions & 2 deletions open_sea_v1/endpoints/tests/test_assets.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from itertools import combinations
from unittest import TestCase

from open_sea_v1.endpoints import AssetsEndpoint, AssetsOrderBy, ClientParams
from open_sea_v1.responses import AssetResponse
from open_sea_v1.endpoints.assets import AssetsEndpoint, AssetsOrderBy, ClientParams
from open_sea_v1.responses.asset import AssetResponse


class TestAssetsRequest(TestCase):
Expand Down
54 changes: 54 additions & 0 deletions open_sea_v1/endpoints/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from itertools import chain
from unittest import TestCase

from open_sea_v1.endpoints.client import ClientParams
from open_sea_v1.endpoints.events import EventsEndpoint, EventType


class TestBaseEndpointClient(TestCase):

@classmethod
def setUpClass(cls) -> None:
cls.max_pages = 2
cls.limit = 5
cls.sample_client = EventsEndpoint(
client_params=ClientParams(max_pages=cls.max_pages, limit=cls.limit),
asset_contract_address="0x76be3b62873462d2142405439777e971754e8e77",
token_id=str(10152),
event_type=EventType.SUCCESSFUL,
)
cls.sample_pages = list(cls.sample_client.get_pages())

def test_remaining_pages_true_if_http_response_is_none(self):
self.sample_client._http_response = None
self.assertTrue(self.sample_client.remaining_pages())

def test_get_pages_resets_processed_pages_and_offset_attr_on_new_calls(self):
for _ in range(2):
next(self.sample_client.get_pages())
self.assertEqual(self.sample_client.processed_pages, 1)
expected_offset_value = self.sample_client.client_params.limit
self.assertEqual(self.sample_client.client_params.offset, expected_offset_value)

def test_get_pages_does_not_append_empty_pages(self):
no_empty_pages = all(not page == list() for page in self.sample_pages)
self.assertTrue(no_empty_pages)

def test_get_pages_max_pages_and_limit_params_works(self):
self.assertLessEqual(len(self.sample_pages), self.max_pages + 1)
for page in self.sample_pages[:-1]:
self.assertEqual(self.limit, len(page))

def test_pagination_works(self):
id_list_1 = [[e.id for e in page] for page in self.sample_client.get_pages()]
id_list_1 = list(chain.from_iterable(id_list_1))
id_list_1.sort(reverse=True)

self.sample_client.client_params = ClientParams(limit=4, offset=0, max_pages=2)
id_list_2 = [[e.id for e in page] for page in self.sample_client.get_pages()]
id_list_2 = list(chain.from_iterable(id_list_2))
id_list_2.sort(reverse=True)

self.assertEqual(len(id_list_2), 12) # updated limit * max_pages+1
self.assertGreater(len(id_list_1), len(id_list_2))
self.assertTrue(id_list_1[i] == id_list_2[i] for i in range(len(id_list_2)))
4 changes: 2 additions & 2 deletions open_sea_v1/endpoints/tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from unittest import TestCase
from datetime import datetime, timedelta

from open_sea_v1.endpoints import EventsEndpoint, EventType, AuctionType
from open_sea_v1.endpoints import ClientParams
from open_sea_v1.endpoints.events import EventsEndpoint, EventType, AuctionType
from open_sea_v1.endpoints.client import ClientParams


class TestEventsEndpoint(TestCase):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
OPENSEA_LISTINGS_V1 = "https://api.opensea.io/wyvern/v1/"


class OpenseaApiEndpoints(str, Enum):
class EndpointURLS(str, Enum):
ASSET = OPENSEA_API_V1 + "asset"
ASSETS = OPENSEA_API_V1 + "assets"
ASSET_CONTRACT = OPENSEA_API_V1 + "asset_contract"
Expand Down
6 changes: 0 additions & 6 deletions open_sea_v1/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +0,0 @@
"""
Exposes classes and objects meant to be used by You!
Import other modules at your own risk, as their location may change.
"""
from open_sea_v1.helpers.ether_converter import EtherConverter, EtherUnit
from open_sea_v1.helpers.extended_classes import ExtendedStrEnum
32 changes: 32 additions & 0 deletions open_sea_v1/helpers/response_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, Any, Union

from open_sea_v1.responses import OpenSeaResponse


@dataclass
class ResponseParser:
"""
Interface for saving and loading OpenseaAPI responses from and to JSON files.
"""
destination: Path
response_type: Type[OpenSeaResponse]

def __post_init__(self):
if not self.destination.exists():
self.destination.parent.mkdir(parents=True, exist_ok=True)

def dump(self, to_parse: Optional[Union[OpenSeaResponse, list[OpenSeaResponse]]]) -> None:
if isinstance(to_parse, list):
the_jsons = [e._json for e in to_parse]
else:
the_jsons = to_parse._json
with open(str(self.destination), 'w') as f:
json.dump(the_jsons, f)

def load(self) -> Any:
with open(str(self.destination), 'r') as f:
parsed_json = json.load(f)
return [self.response_type(collection) for collection in parsed_json]
11 changes: 0 additions & 11 deletions open_sea_v1/responses/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
"""
Exposes classes and objects meant to be used by You!
Import other modules at your own risk, as their location may change.
"""
from open_sea_v1.responses.response_asset import _AssetResponse as AssetResponse
from open_sea_v1.responses.response_asset import _CollectionResponse as CollectionResponse

from open_sea_v1.responses.response_event import _EventReponse as EventResponse

from open_sea_v1.responses.response_abc import _OpenSeaResponse as OpenSeaAPIResponse
from open_sea_v1.responses.response_parser import _ResponseParser as ResponseParser
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABC


class _OpenSeaResponse(ABC):
class BaseResponse(ABC):
"""Parent class for OpenSea API Responses."""

def __init__(self, _json: dict = None):
Expand Down
Loading

0 comments on commit 30525c7

Please sign in to comment.