From 7887454f388dcfe8b970570581dc17fa4edbb1a6 Mon Sep 17 00:00:00 2001 From: dehidehidehi Date: Tue, 3 Aug 2021 12:52:13 +0200 Subject: [PATCH] Implemented EventsEndpoint --- open_sea_v1/endpoints/__init__.py | 6 +- open_sea_v1/endpoints/endpoint_abc.py | 25 ++- open_sea_v1/endpoints/endpoint_assets.py | 30 +--- open_sea_v1/endpoints/endpoint_client.py | 18 +- open_sea_v1/endpoints/endpoint_events.py | 154 ++++++++++++++++++ open_sea_v1/endpoints/tests/test_assets.py | 2 +- open_sea_v1/endpoints/tests/test_events.py | 100 ++++++++++++ open_sea_v1/helpers/__init__.py | 0 open_sea_v1/helpers/ether_converter.py | 62 +++++++ open_sea_v1/helpers/extended_classes.py | 9 + open_sea_v1/helpers/tests/__init__.py | 0 .../helpers/tests/test_ether_converter.py | 31 ++++ open_sea_v1/responses/__init__.py | 5 +- open_sea_v1/responses/response__base.py | 4 + .../{asset_obj.py => response_asset.py} | 28 +++- ...llection_obj.py => response_collection.py} | 4 +- open_sea_v1/responses/response_event.py | 55 +++++++ .../responses/tests/_response_helpers.py | 24 +++ open_sea_v1/responses/tests/test_asset_obj.py | 29 ---- .../responses/tests/test_reponse_event.py | 21 +++ .../responses/tests/test_response_asset.py | 20 +++ 21 files changed, 554 insertions(+), 73 deletions(-) create mode 100644 open_sea_v1/endpoints/endpoint_events.py create mode 100644 open_sea_v1/endpoints/tests/test_events.py create mode 100644 open_sea_v1/helpers/__init__.py create mode 100644 open_sea_v1/helpers/ether_converter.py create mode 100644 open_sea_v1/helpers/extended_classes.py create mode 100644 open_sea_v1/helpers/tests/__init__.py create mode 100644 open_sea_v1/helpers/tests/test_ether_converter.py create mode 100644 open_sea_v1/responses/response__base.py rename open_sea_v1/responses/{asset_obj.py => response_asset.py} (80%) rename open_sea_v1/responses/{collection_obj.py => response_collection.py} (96%) create mode 100644 open_sea_v1/responses/response_event.py create mode 100644 open_sea_v1/responses/tests/_response_helpers.py delete mode 100644 open_sea_v1/responses/tests/test_asset_obj.py create mode 100644 open_sea_v1/responses/tests/test_reponse_event.py create mode 100644 open_sea_v1/responses/tests/test_response_asset.py diff --git a/open_sea_v1/endpoints/__init__.py b/open_sea_v1/endpoints/__init__.py index a22cb32..d081594 100644 --- a/open_sea_v1/endpoints/__init__.py +++ b/open_sea_v1/endpoints/__init__.py @@ -3,4 +3,8 @@ Import other modules at your own risk, as their location may change. """ from open_sea_v1.endpoints.endpoint_assets import _AssetsEndpoint as AssetsEndpoint -from open_sea_v1.endpoints.endpoint_assets import _AssetsOrderBy as AssetsOrderBy \ No newline at end of file +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 diff --git a/open_sea_v1/endpoints/endpoint_abc.py b/open_sea_v1/endpoints/endpoint_abc.py index 40cb88a..4feb9e6 100644 --- a/open_sea_v1/endpoints/endpoint_abc.py +++ b/open_sea_v1/endpoints/endpoint_abc.py @@ -1,14 +1,17 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Union from requests import Response +from open_sea_v1.responses.response__base import _OpenSeaAPIResponse + class BaseOpenSeaEndpoint(ABC): + @property @abstractmethod - def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: - """Call to super().get_request passing url and _request_params.""" + def __post_init__(self): + """Using post_init to run param validation""" @property @abstractmethod @@ -23,14 +26,24 @@ def url(self) -> str: @property @abstractmethod def _request_params(self) -> dict: - """Dictionnary of _request_params to pass into the get_request.""" + """Dictionnary of _request_params to pass into the _get_request.""" @property @abstractmethod - def validate_request_params(self) -> None: + def _validate_request_params(self) -> None: """""" @property @abstractmethod - def response(self) -> list[dict]: + def response(self) -> Union[list[_OpenSeaAPIResponse], _OpenSeaAPIResponse]: """Parsed JSON dictionnary from HTTP Response.""" + + @property + @abstractmethod + def http_response(self) -> Optional[Response]: + """HTTP Response from Opensea API.""" + + @abstractmethod + def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: + """Call to super()._get_request passing url and _request_params.""" + diff --git a/open_sea_v1/endpoints/endpoint_assets.py b/open_sea_v1/endpoints/endpoint_assets.py index 0f0a1d5..099499c 100644 --- a/open_sea_v1/endpoints/endpoint_assets.py +++ b/open_sea_v1/endpoints/endpoint_assets.py @@ -2,12 +2,10 @@ from enum import Enum from typing import Optional -from requests import Response - -from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints -from open_sea_v1.endpoints.endpoint_client import OpenSeaClient from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint -from open_sea_v1.responses.asset_obj import _AssetResponse +from open_sea_v1.endpoints.endpoint_client import OpenSeaClient +from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints +from open_sea_v1.responses.response_asset import _AssetResponse class _AssetsOrderBy(str, Enum): @@ -28,8 +26,6 @@ class _AssetsEndpoint(OpenSeaClient, BaseOpenSeaEndpoint): Parameters ---------- - width: - width of the snake owner: The address of the owner of the assets @@ -72,17 +68,11 @@ class _AssetsEndpoint(OpenSeaClient, BaseOpenSeaEndpoint): limit: int = 20 def __post_init__(self): - self.validate_request_params() - self._response: Optional[Response] = None - - @property - def http_response(self): - self._validate_response_property() - return self._response + self._validate_request_params() @property def response(self) -> list[_AssetResponse]: - self._validate_response_property() + self._assert_get_request_was_called_before_accessing_this_property() assets_json = self._response.json()['assets'] assets = [_AssetResponse(asset_json) for asset_json in assets_json] return assets @@ -92,7 +82,7 @@ def url(self): return OpenseaApiEndpoints.ASSETS.value def get_request(self, *args, **kwargs): - self._response = super().get_request(self.url, **self._request_params) + self._response = self._get_request(self.url, **self._request_params) @property def _request_params(self) -> dict[dict]: @@ -103,17 +93,13 @@ def _request_params(self) -> dict[dict]: ) return dict(api_key=self.api_key, params=params) - def validate_request_params(self) -> None: + def _validate_request_params(self) -> None: self._validate_mandatory_params() self._validate_asset_contract_addresses() self._validate_order_direction() self._validate_order_by() self._validate_limit() - def _validate_response_property(self): - if self._response is None: - raise AttributeError('You must call self.request prior to accessing self.response') - def _validate_mandatory_params(self): mandatory = self.owner, self.token_ids, self.asset_contract_address, self.asset_contract_addresses, self.collection if all((a is None for a in mandatory)): @@ -123,7 +109,7 @@ def _validate_mandatory_params(self): def _validate_asset_contract_addresses(self): if self.asset_contract_address and self.asset_contract_addresses: raise ValueError( - "You cannot simultaneously get_request for a single contract_address and a list of contract_addresses." + "You cannot simultaneously _get_request for a single contract_address and a list of contract_addresses." ) if self.token_ids and not (self.asset_contract_address or self.asset_contract_addresses): diff --git a/open_sea_v1/endpoints/endpoint_client.py b/open_sea_v1/endpoints/endpoint_client.py index b74e999..dc642ac 100644 --- a/open_sea_v1/endpoints/endpoint_client.py +++ b/open_sea_v1/endpoints/endpoint_client.py @@ -1,9 +1,12 @@ +from typing import Optional + from requests import Response, request class OpenSeaClient: api_key = None + _response: Optional[Response] = None @property def http_headers(self) -> dict: @@ -12,15 +15,24 @@ def http_headers(self) -> dict: {"X-API-Key" : self.api_key} if self.api_key else dict(), } - def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: + def _get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: """ - Automatically passes in API key in HTTP get_request headers. + Automatically passes in API key in HTTP _get_request headers. """ if 'api_key' in kwargs: self.api_key = kwargs.pop('api_key') updated_kwargs = kwargs | self.http_headers return request(method, url, **updated_kwargs) + @property + def http_response(self): + self._assert_get_request_was_called_before_accessing_this_property() + return self._response + + def _assert_get_request_was_called_before_accessing_this_property(self): + if self._response is None: + raise AttributeError('You must call self.request prior to accessing self.response') + # def collections(self, *, asset_owner: Optional[str] = None, offset: int, limit: int) -> OpenseaCollections: # """ # Use this endpoint to fetch collections and dapps that OpenSea shows on opensea.io, @@ -45,7 +57,7 @@ def get_request(self, url: str, method: str = 'GET', **kwargs) -> Response: # def _collections(self, **_request_params) -> Response: # """Returns HTTPResponse object.""" # url = OpenseaApiEndpoints.COLLECTIONS.value - # return self.get_request("GET", url, _request_params=_request_params) + # return self._get_request("GET", url, _request_params=_request_params) # # def asset(self, asset_contract_address: str, token_id: str, account_address: Optional[str] = None) -> OpenseaAsset: # """ diff --git a/open_sea_v1/endpoints/endpoint_events.py b/open_sea_v1/endpoints/endpoint_events.py new file mode 100644 index 0000000..6c42815 --- /dev/null +++ b/open_sea_v1/endpoints/endpoint_events.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +from open_sea_v1.endpoints.endpoint_abc import BaseOpenSeaEndpoint +from open_sea_v1.endpoints.endpoint_client import OpenSeaClient +from open_sea_v1.endpoints.endpoint_urls import OpenseaApiEndpoints +from open_sea_v1.helpers.extended_classes import ExtendedStrEnum +from open_sea_v1.responses import EventResponse + + +class EventType(ExtendedStrEnum): + """ + The event type to filter. Can be created for new auctions, successful for sales, cancelled, bid_entered, bid_withdrawn, transfer, or approve + """ + CREATED = 'created' + SUCCESSFUL = 'successful' + CANCELLED = 'cancelled' + BID_ENTERED = 'bid_entered' + BID_WITHDRAWN = 'bid_withdrawn' + TRANSFER = 'transfer' + APPROVE = 'approve' + + +class AuctionType(ExtendedStrEnum): + """ + Filter by an auction type. Can be english for English Auctions, dutch for fixed-price and declining-price sell orders (Dutch Auctions), or min-price for CryptoPunks bidding auctions. + """ + ENGLISH = 'english' + DUTCH = 'dutch' + MIN_PRICE = 'min-price' + + +@dataclass +class _EventsEndpoint(OpenSeaClient, BaseOpenSeaEndpoint): + """ + Opensea API Events Endpoint + + Parameters + ---------- + + offset: + Offset for pagination + + limit: + Limit for pagination + + asset_contract_address: + The NFT contract address for the assets for which to show events + + event_type: + The event type to filter. Can be created for new auctions, successful for sales, cancelled, bid_entered, bid_withdrawn, transfer, or approve + + only_opensea: + Restrict to events on OpenSea auctions. Can be true or false + + auction_type: + Filter by an auction type. Can be english for English Auctions, dutch for fixed-price and declining-price sell orders (Dutch Auctions), or min-price for CryptoPunks bidding auctions. + + occurred_before: + Only show events listed before this datetime. + + occurred_after: + Only show events listed after this datetime. + + api_key: + Optional Opensea API key, if you have one. + + :return: Parsed JSON + """ + offset: int + limit: int + asset_contract_address: str + event_type: EventType + only_opensea: bool + collection_slug: Optional[str] = None + token_id: Optional[str] = None + account_address: Optional[str] = None + auction_type: Optional[AuctionType] = None + occurred_before: Optional[datetime] = None + occurred_after: Optional[datetime] = None + api_key: Optional[str] = None + + def __post_init__(self): + self._validate_request_params() + + @property + def url(self) -> str: + return OpenseaApiEndpoints.EVENTS.value + + @property + def _request_params(self) -> dict: + params = dict(offset=self.offset, limit=self.limit, asset_contract_address=self.asset_contract_address, event_type=self.event_type, only_opensea=self.only_opensea, collection_slug=self.collection_slug, token_id=self.token_id, account_address=self.account_address, auction_type=self.auction_type, occurred_before=self.occurred_before, occurred_after=self.occurred_after) + return dict(api_key=self.api_key, params=params) + + def get_request(self, **kwargs): + self._response = self._get_request(self.url, **self._request_params) + + @property + def response(self) -> list[EventResponse]: + self._assert_get_request_was_called_before_accessing_this_property() + events_json = self._response.json()['asset_events'] + events = [EventResponse(event) for event in events_json] + return events + + def _validate_request_params(self) -> None: + self._validate_param_auction_type() + self._validate_param_event_type() + self._validate_params_occurred_before_and_occurred_after() + + def _validate_param_event_type(self) -> None: + if not isinstance(self.event_type, (str, EventType)): + raise TypeError('Invalid event_type type. Must be str or EventType Enum.', f"{self.event_type=}") + + if self.event_type not in EventType.list(): + raise ValueError('Invalid event_type value. Must be str value from EventType Enum.', f"{self.event_type=}") + + def _validate_param_auction_type(self) -> None: + if self.auction_type is None: + return + + if not isinstance(self.auction_type, (str, AuctionType)): + raise TypeError('Invalid auction_type type. Must be str or AuctionType Enum.', f"{self.auction_type=}") + + if self.auction_type not in AuctionType.list(): + raise ValueError('Invalid auction_type value. Must be str value from AuctionType Enum.', + f"{self.auction_type=}") + + def _validate_params_occurred_before_and_occurred_after(self) -> None: + self._validate_param_occurred_before() + self._validate_param_occurred_after() + if self.occurred_after and self.occurred_before: + self._assert_param_occurred_before_after_cannot_be_same_value() + self._assert_param_occurred_before_cannot_be_higher_than_occurred_after() + + def _validate_param_occurred_before(self) -> None: + if not isinstance(self.occurred_before, (type(None), datetime)): + raise TypeError('Invalid occurred_before type. Must be instance of datetime.', + f'{type(self.occurred_before)=}') + + def _validate_param_occurred_after(self) -> None: + if not isinstance(self.occurred_after, (type(None), datetime)): + raise TypeError('Invalid occurred_after type. Must be instance of datetime.', + f'{type(self.occurred_after)=}') + + def _assert_param_occurred_before_after_cannot_be_same_value(self) -> None: + if self.occurred_after == self.occurred_before: + raise ValueError('Params occurred_after and occurred_before may not have the same value.', + f"{self.occurred_before=}, {self.occurred_after=}") + + def _assert_param_occurred_before_cannot_be_higher_than_occurred_after(self) -> None: + if not self.occurred_after < self.occurred_before: + raise ValueError('Param occurred_before cannot be higher than param occurred_after.', + f"{self.occurred_before=}, {self.occurred_after=}") \ No newline at end of file diff --git a/open_sea_v1/endpoints/tests/test_assets.py b/open_sea_v1/endpoints/tests/test_assets.py index cbcc568..47e8b86 100644 --- a/open_sea_v1/endpoints/tests/test_assets.py +++ b/open_sea_v1/endpoints/tests/test_assets.py @@ -2,7 +2,7 @@ from unittest import TestCase from open_sea_v1.endpoints.endpoint_assets import _AssetsEndpoint, _AssetsOrderBy -from open_sea_v1.responses.asset_obj import _AssetResponse +from open_sea_v1.responses.response_asset import _AssetResponse class TestAssetsRequest(TestCase): diff --git a/open_sea_v1/endpoints/tests/test_events.py b/open_sea_v1/endpoints/tests/test_events.py new file mode 100644 index 0000000..c4fb8ba --- /dev/null +++ b/open_sea_v1/endpoints/tests/test_events.py @@ -0,0 +1,100 @@ +from unittest import TestCase +from datetime import datetime, timedelta + +from open_sea_v1.endpoints import EventsEndpoint, EventType + + +class TestEventsEndpoint(TestCase): + sample_contract = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" # punk + events_default_kwargs = dict( + offset=0, limit=1, asset_contract_address=sample_contract, + only_opensea=False, event_type=EventType.SUCCESSFUL, + ) + + @staticmethod + def create_and_get(**kwargs): + endpoint = EventsEndpoint(**kwargs) + endpoint.get_request() + return endpoint.response + + def test_param_event_type_filters_properly(self): + updated_kwargs = self.events_default_kwargs | dict(limit=5) + punks_events = self.create_and_get(**updated_kwargs) + self.assertTrue(all(e.event_type == EventType.SUCCESSFUL for e in punks_events)) + + def test_param_event_type_raises_if_not_from_event_type_enum_values(self): + updated_kwargs = self.events_default_kwargs | dict(event_type='randomstr') + self.assertRaises((ValueError, TypeError), self.create_and_get, **updated_kwargs) + + def test_param_auction_type_filters_properly(self): + updated_kwargs = self.events_default_kwargs | dict(event_type=0.0) + self.assertRaises((ValueError, TypeError), self.create_and_get, **updated_kwargs) + + def test_param_auction_type_raises_if_not_from_auction_type_enum_values(self): + updated_kwargs = self.events_default_kwargs | dict(auction_type='randomstr') + self.assertRaises((ValueError, TypeError), self.create_and_get, **updated_kwargs) + + def test_param_auction_type_raises_if_not_isinstance_of_str(self): + updated_kwargs = self.events_default_kwargs | dict(auction_type=0.0) + self.assertRaises((ValueError, TypeError), self.create_and_get, **updated_kwargs) + + def test_param_auction_type_does_not_raise_if_is_none(self): + updated_kwargs = self.events_default_kwargs | dict(auction_type=None) + self.create_and_get(**updated_kwargs) + + def test_param_only_opensea_true_filters_properly(self): + updated_kwargs = self.events_default_kwargs | dict(only_opensea=True, limit=2) + events = self.create_and_get(**updated_kwargs) + self.assertTrue(all('opensea.io' in event.asset.permalink for event in events)) + + def test_param_only_opensea_false_does_not_filter(self): + """ + Have no idea how to test this param. + """ + # updated_kwargs = self.events_default_kwargs | dict(only_opensea=False, offset=1, limit=100) + # events = self.create_and_get(**updated_kwargs) + # self.assertTrue(any('opensea.io' not in event.asset.permalink for event in events)) + pass + + def test_param_occurred_before_raises_exception_if_not_datetime_instances(self): + updated_kwargs = self.events_default_kwargs | dict(occurred_before=True) + self.assertRaises(TypeError, self.create_and_get, **updated_kwargs) + + def test_param_occurred_before_and_after_raises_exception_if_are_equal_values(self): + dt_now = datetime.now() + occurred_params = dict(occurred_before=dt_now, occurred_after=dt_now) + updated_kwargs = self.events_default_kwargs | occurred_params + self.assertRaises(ValueError, self.create_and_get, **updated_kwargs) + + def test_param_occurred_before_and_after_does_not_raise_if_both_are_none(self): + updated_kwargs = self.events_default_kwargs | dict(occurred_before=None, occurred_after=None) + self.create_and_get(**updated_kwargs) + + def test_param_occurred_after_cannot_be_higher_than_occurred_before(self): + occurred_before = datetime.now() + occurred_after = occurred_before + timedelta(microseconds=1) + occurred_params = dict(occurred_before=occurred_before, occurred_after=occurred_after) + updated_kwargs = self.events_default_kwargs | occurred_params + self.assertRaises(ValueError, self.create_and_get, **updated_kwargs) + + def test_param_occurred_after_filters_properly(self): + occurred_after = datetime(year=2021, month=8, day=1) + updated_kwargs = self.events_default_kwargs | dict(occurred_after=occurred_after, limit=5) + events = self.create_and_get(**updated_kwargs) + transaction_datetimes = [datetime.fromisoformat(event.transaction['timestamp']) for event in events] + self.assertTrue(all(trans_date >= occurred_after for trans_date in transaction_datetimes)) + + def test_param_occurred_before_filters_properly(self): + occurred_before = datetime(year=2021, month=8, day=1) + updated_kwargs = self.events_default_kwargs | dict(occurred_before=occurred_before, limit=5) + events = self.create_and_get(**updated_kwargs) + transaction_datetimes = [datetime.fromisoformat(event.transaction['timestamp']) for event in events] + self.assertTrue(all(trans_date < occurred_before for trans_date in transaction_datetimes)) + + def test_params_occurred_before_after_work_together(self): + occurred_after = datetime(year=2021, month=7, day=30) + occurred_before = datetime(year=2021, month=8, day=2) + updated_kwargs = self.events_default_kwargs | dict(occurred_after=occurred_after, occurred_before=occurred_before, limit=5) + events = self.create_and_get(**updated_kwargs) + transaction_datetimes = [datetime.fromisoformat(event.transaction['timestamp']) for event in events] + self.assertTrue(all(occurred_after <= trans_date <= occurred_before for trans_date in transaction_datetimes)) \ No newline at end of file diff --git a/open_sea_v1/helpers/__init__.py b/open_sea_v1/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/helpers/ether_converter.py b/open_sea_v1/helpers/ether_converter.py new file mode 100644 index 0000000..7154ff4 --- /dev/null +++ b/open_sea_v1/helpers/ether_converter.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass +from enum import IntEnum +from typing import Union + + +class EtherUnit(IntEnum): + """ + Ether sub-units quantified in Wei. + """ + WEI = 1 + KWEI = 1_000 + MWEI = 1_000_000 + GWEI = 1_000_000_000 + TWEI = 1_000_000_000_000 + PWEI = 1_000_000_000_000_000 + ETHER = 1_000_000_000_000_000_000 + + +@dataclass +class EtherConverter: + """ + Convenience class whic helps convert Ether and it's sub-units (gwei, twei etc.) into other sub-units. + """ + quantity: Union[str, int, float] + unit: EtherUnit + + def __post_init__(self): + if isinstance(self.quantity, str): + self.quantity = float(self.quantity) + + def convert_to(self, unit: EtherUnit) -> float: + if unit == self.unit: + return self.quantity + return self.unit / unit * self.quantity + + @property + def ether(self): + return self.convert_to(EtherUnit.ETHER) + + @property + def pwei(self): + return self.convert_to(EtherUnit.PWEI) + + @property + def twei(self): + return self.convert_to(EtherUnit.TWEI) + + @property + def gwei(self): + return self.convert_to(EtherUnit.GWEI) + + @property + def mwei(self): + return self.convert_to(EtherUnit.MWEI) + + @property + def kwei(self): + return self.convert_to(EtherUnit.KWEI) + + @property + def wei(self): + return self.convert_to(EtherUnit.WEI) \ No newline at end of file diff --git a/open_sea_v1/helpers/extended_classes.py b/open_sea_v1/helpers/extended_classes.py new file mode 100644 index 0000000..278497e --- /dev/null +++ b/open_sea_v1/helpers/extended_classes.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ExtendedStrEnum(str, Enum): + """Adds list method which will list all values associated with children instances.""" + + @classmethod + def list(cls) -> list[str]: + return list(map(lambda c: c.value, cls)) \ No newline at end of file diff --git a/open_sea_v1/helpers/tests/__init__.py b/open_sea_v1/helpers/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_sea_v1/helpers/tests/test_ether_converter.py b/open_sea_v1/helpers/tests/test_ether_converter.py new file mode 100644 index 0000000..3831de3 --- /dev/null +++ b/open_sea_v1/helpers/tests/test_ether_converter.py @@ -0,0 +1,31 @@ +from unittest import TestCase + +from open_sea_v1.helpers.ether_converter import EtherConverter, EtherUnit + + +class TestEtherUnitConverter(TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.converter = EtherConverter(quantity=35, unit=EtherUnit.GWEI) + + def test_to_wei(self): + self.assertEqual(35_000_000_000, self.converter.wei) + + def test_to_kwei(self): + self.assertEqual(35_000_000, self.converter.kwei) + + def test_to_mwei(self): + self.assertEqual(35_000, self.converter.mwei) + + def test_to_gwei(self): + self.assertEqual(35, self.converter.gwei) + + def test_to_twei(self): + self.assertEqual(0.035, self.converter.twei) + + def test_to_pwei(self): + self.assertEqual(0.000_035, self.converter.pwei) + + def test_to_ether(self): + self.assertEqual(0.000_000_035, self.converter.ether) diff --git a/open_sea_v1/responses/__init__.py b/open_sea_v1/responses/__init__.py index f33d30f..1834d13 100644 --- a/open_sea_v1/responses/__init__.py +++ b/open_sea_v1/responses/__init__.py @@ -2,5 +2,6 @@ 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.asset_obj import _AssetResponse as AssetResponse -from open_sea_v1.responses.asset_obj import _CollectionResponse as CollectionResponse +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 diff --git a/open_sea_v1/responses/response__base.py b/open_sea_v1/responses/response__base.py new file mode 100644 index 0000000..4e55bb8 --- /dev/null +++ b/open_sea_v1/responses/response__base.py @@ -0,0 +1,4 @@ + +class _OpenSeaAPIResponse: + """Parent class for OpenSea API Responses.""" + _json: dict = None diff --git a/open_sea_v1/responses/asset_obj.py b/open_sea_v1/responses/response_asset.py similarity index 80% rename from open_sea_v1/responses/asset_obj.py rename to open_sea_v1/responses/response_asset.py index 6d2a925..716baab 100644 --- a/open_sea_v1/responses/asset_obj.py +++ b/open_sea_v1/responses/response_asset.py @@ -3,7 +3,8 @@ """ from dataclasses import dataclass -from open_sea_v1.responses.collection_obj import _CollectionResponse +from open_sea_v1.responses.response__base import _OpenSeaAPIResponse +from open_sea_v1.responses.response_collection import _CollectionResponse @dataclass @@ -73,13 +74,17 @@ def __post_init__(self): @dataclass -class _AssetResponse: +class _AssetResponse(_OpenSeaAPIResponse): _json: dict def __str__(self) -> str: return f"({_AssetResponse.__name__}, id={self.token_id.zfill(5)}, name={self.name})" def __post_init__(self): + self._set_common_attrs() + self._set_optional_attrs() + + def _set_common_attrs(self): self.token_id = self._json["token_id"] self.num_sales = self._json["num_sales"] self.background_color = self._json["background_color"] @@ -95,12 +100,19 @@ def __post_init__(self): self.permalink = self._json["permalink"] self.decimals = self._json["decimals"] self.token_metadata = self._json["token_metadata"] - self.sell_orders = self._json["sell_orders"] - self.top_bid = self._json["top_bid"] - self.listing_date = self._json["listing_date"] - self.is_presale = self._json["is_presale"] - self.transfer_fee_payment_token = self._json["transfer_fee_payment_token"] - self.transfer_fee = self._json["transfer_fee"] + self.id = self._json["id"] + + def _set_optional_attrs(self): + """ + Most asset responses are alike, but some are returned with less information. + To avoid raising KeyErrors, we will use the .get method when setting these attributes. + """ + self.transfer_fee = self._json.get("transfer_fee") + self.transfer_fee_payment_token = self._json.get("transfer_fee_payment_token") + self.is_presale = self._json.get("is_presale") + self.listing_date = self._json.get("listing_date") + self.top_bid = self._json.get("top_bid") + self.sell_orders = self._json.get("sell_orders") @property def asset_contract(self) -> _Contract: diff --git a/open_sea_v1/responses/collection_obj.py b/open_sea_v1/responses/response_collection.py similarity index 96% rename from open_sea_v1/responses/collection_obj.py rename to open_sea_v1/responses/response_collection.py index c568dc8..1ca584e 100644 --- a/open_sea_v1/responses/collection_obj.py +++ b/open_sea_v1/responses/response_collection.py @@ -3,6 +3,8 @@ """ from dataclasses import dataclass +from open_sea_v1.responses.response__base import _OpenSeaAPIResponse + @dataclass class _CollectionStats: @@ -36,7 +38,7 @@ def __post_init__(self): @dataclass -class _CollectionResponse: +class _CollectionResponse(_OpenSeaAPIResponse): _json: dict def __str__(self) -> str: diff --git a/open_sea_v1/responses/response_event.py b/open_sea_v1/responses/response_event.py new file mode 100644 index 0000000..394aba3 --- /dev/null +++ b/open_sea_v1/responses/response_event.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass + +from open_sea_v1.responses import AssetResponse +from open_sea_v1.responses.response__base import _OpenSeaAPIResponse + + +@dataclass +class _EventReponse(_OpenSeaAPIResponse): + _json: dict + + def __str__(self) -> str: + return f"{self.event_type=}, {self.total_price=}" + + def __post_init__(self): + self.approved_account = self._json['approved_account'] + self.asset_bundle = self._json['asset_bundle'] + self.auction_type = self._json['auction_type'] + # self.big_amount = self._json['big_amount'] + self.collection_slug = self._json['collection_slug'] + self.contract_address = self._json['contract_address'] + self.created_date = self._json['created_date'] + self.custom_event_name = self._json['custom_event_name'] + self.dev_fee_payment_event = self._json['dev_fee_payment_event'] + self.duration = self._json['duration'] + self.ending_price = self._json['ending_price'] + self.event_type = self._json['event_type'] + self.from_account = self._json['from_account'] + self.id = self._json['id'] + self.owner_account = self._json['owner_account'] + self.quantity = self._json['quantity'] + self.starting_price = self._json['starting_price'] + self.to_account = self._json['to_account'] + self.total_price = self._json['total_price'] + self.bid_amount = self._json['bid_amount'] + + @property + def asset(self) -> AssetResponse: + return AssetResponse(self._json['asset']) + + @property + def payment_token(self) -> dict: + return self._json['payment_token'] + + @property + def seller(self) -> dict: + return self._json['seller'] + + @property + def transaction(self) -> dict: + return self._json['transaction'] + + @property + def winner_account(self) -> dict: + return self._json['winner_account'] + diff --git a/open_sea_v1/responses/tests/_response_helpers.py b/open_sea_v1/responses/tests/_response_helpers.py new file mode 100644 index 0000000..7a5acd3 --- /dev/null +++ b/open_sea_v1/responses/tests/_response_helpers.py @@ -0,0 +1,24 @@ +from unittest import TestCase + +from open_sea_v1.responses.response__base import _OpenSeaAPIResponse + + +class ResponseTestHelper(TestCase): + + @classmethod + def create_and_get(cls, endpoint_client, **kwargs) -> list[_OpenSeaAPIResponse]: + """Shortcut""" + client = endpoint_client(**kwargs) + client.get_request() + return client.response + + @staticmethod + def assert_attributes_do_not_raise_unexpected_exceptions(target_obj): + attrs = [n for n in dir(target_obj) if not n.startswith('__')] + for a in attrs: + getattr(target_obj, a) + + @staticmethod + def assert_no_missing_class_attributes_from_original_json_keys(response_obj, json): + for key in json: + getattr(response_obj, key) diff --git a/open_sea_v1/responses/tests/test_asset_obj.py b/open_sea_v1/responses/tests/test_asset_obj.py deleted file mode 100644 index 03471fd..0000000 --- a/open_sea_v1/responses/tests/test_asset_obj.py +++ /dev/null @@ -1,29 +0,0 @@ -from unittest import TestCase - -from open_sea_v1.endpoints import AssetsEndpoint -from open_sea_v1.responses import AssetResponse - - -class TestAssetObj(TestCase): - sample_contract = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" # punk - default_asset_params = dict(token_ids=[5, 6, 7], asset_contract_address=sample_contract) - - @classmethod - def setUpClass(cls) -> None: - params = cls.default_asset_params | dict(token_ids=[1, 14, 33]) - cls.assets = cls.create_and_get(**params) - - @classmethod - def create_and_get(cls, **kwargs) -> list[AssetResponse]: - """Shortcut""" - client = AssetsEndpoint(**kwargs) - client.get_request() - return client.response - - def test_attributes_do_not_raise_unexpected_exceptions(self): - target_obj = self.assets[0] - attrs = [n for n in dir(target_obj) if not n.startswith('__')] - for a in attrs: - getattr(target_obj, a) - - diff --git a/open_sea_v1/responses/tests/test_reponse_event.py b/open_sea_v1/responses/tests/test_reponse_event.py new file mode 100644 index 0000000..5071af4 --- /dev/null +++ b/open_sea_v1/responses/tests/test_reponse_event.py @@ -0,0 +1,21 @@ +from open_sea_v1.endpoints import EventsEndpoint, EventType +from open_sea_v1.responses.tests._response_helpers import ResponseTestHelper + + +class TestEventsObj(ResponseTestHelper): + sample_contract = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" # punk + events_default_kwargs = dict( + offset=0, limit=1, asset_contract_address=sample_contract, + only_opensea=False, event_type=EventType.SUCCESSFUL, + ) + + @classmethod + def setUpClass(cls) -> None: + cls.events = cls.create_and_get(EventsEndpoint, **cls.events_default_kwargs) + cls.event = cls.events[0] + + def test_attributes_do_not_raise_unexpected_exceptions(self): + self.assert_attributes_do_not_raise_unexpected_exceptions(self.event) + + def test_no_missing_class_attributes_from_original_json_keys(self): + self.assert_no_missing_class_attributes_from_original_json_keys(response_obj=self.event, json=self.event._json) diff --git a/open_sea_v1/responses/tests/test_response_asset.py b/open_sea_v1/responses/tests/test_response_asset.py new file mode 100644 index 0000000..f268cb8 --- /dev/null +++ b/open_sea_v1/responses/tests/test_response_asset.py @@ -0,0 +1,20 @@ +from open_sea_v1.endpoints import AssetsEndpoint +from open_sea_v1.responses.tests._response_helpers import ResponseTestHelper + + +class TestAssetObj(ResponseTestHelper): + sample_contract = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" # punk + default_asset_params = dict(token_ids=[5, 6, 7], asset_contract_address=sample_contract) + + @classmethod + def setUpClass(cls) -> None: + params = cls.default_asset_params | dict(token_ids=[1, 14, 33]) + cls.assets = cls.create_and_get(AssetsEndpoint, **params) + cls.asset = cls.assets[0] + + def test_attributes_do_not_raise_unexpected_exceptions(self): + self.assert_attributes_do_not_raise_unexpected_exceptions(self.asset) + + def test_no_missing_class_attributes_from_original_json_keys(self): + self.assert_no_missing_class_attributes_from_original_json_keys(response_obj=self.asset, json=self.asset._json) +