diff --git a/offchain/metadata/parsers/collection/hashmasks.py b/offchain/metadata/parsers/collection/hashmasks.py index 7781333..07646ab 100644 --- a/offchain/metadata/parsers/collection/hashmasks.py +++ b/offchain/metadata/parsers/collection/hashmasks.py @@ -117,7 +117,7 @@ def parse_metadata(self, token: Token, raw_data: dict, *args, **kwargs) -> Optio additional_fields=self.get_additional_fields(raw_data=raw_data), ) - async def gen_parse_metadata(self, token: Token, raw_data: dict, *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 + async def _gen_parse_metadata_impl(self, token: Token, raw_data: dict, *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 token.uri = f"https://hashmap.azurewebsites.net/getMask/{token.token_id}" raw_data, mime_type_and_size, name, image = await asyncio.gather( diff --git a/offchain/metadata/parsers/collection/punks.py b/offchain/metadata/parsers/collection/punks.py index 313b6f1..9d5eae8 100644 --- a/offchain/metadata/parsers/collection/punks.py +++ b/offchain/metadata/parsers/collection/punks.py @@ -1,17 +1,18 @@ +import asyncio from typing import Optional +from urllib.parse import quote from offchain.constants.addresses import CollectionAddress from offchain.metadata.models.metadata import ( - Metadata, - MediaDetails, Attribute, + MediaDetails, + Metadata, MetadataField, MetadataFieldType, ) from offchain.metadata.models.token import Token from offchain.metadata.parsers.collection.collection_parser import CollectionParser from offchain.metadata.registries.parser_registry import ParserRegistry -from urllib.parse import quote @ParserRegistry.register @@ -36,6 +37,19 @@ def make_call(self, index: int, function_sig: str) -> Optional[str]: return results[0] + async def gen_call(self, index: int, function_sig: str) -> Optional[str]: + results = await self.contract_caller.rpc.async_reader.gen_call_single_function_single_address_many_args( + address=CollectionAddress.PUNKS_DATA, + function_sig=function_sig, + return_type=["string"], + args=[[index]], + ) + + if len(results) < 1: + return None + + return results[0] + def get_image(self, index: int) -> Optional[MediaDetails]: raw_uri = self.make_call(index, "punkImageSvg(uint16)") image_uri = self.encode_uri_data(raw_uri) # type: ignore[arg-type] @@ -43,6 +57,13 @@ def get_image(self, index: int) -> Optional[MediaDetails]: uri=image_uri, size=None, sha256=None, mime_type="image/svg+xml" ) # noqa: E501 + async def gen_image(self, index: int) -> Optional[MediaDetails]: + raw_uri = await self.gen_call(index, "punkImageSvg(uint16)") + image_uri = self.encode_uri_data(raw_uri) # type: ignore[arg-type] + return MediaDetails( + uri=image_uri, size=None, sha256=None, mime_type="image/svg+xml" + ) # noqa: E501 + def parse_additional_fields(self, raw_data: dict) -> list[MetadataField]: # type: ignore[type-arg] # noqa: E501 additional_fields = [] if (external_url := raw_data.get("external_url")) is not None: @@ -88,6 +109,28 @@ def parse_attributes(self, token_id: int) -> list[Attribute]: return attributes + async def gen_parse_attributes(self, token_id: int) -> list[Attribute]: + attributes = [] + + punk_attributes = (await self.gen_call(token_id, "punkAttributes(uint16)")).split(", ") # type: ignore[union-attr] # noqa: E501 + + type_attribute = Attribute( + trait_type="Type", + value=punk_attributes[0], + display_type=None, + ) + attributes.append(type_attribute) + + for value in punk_attributes[1:]: + attribute = Attribute( + trait_type="Accessory", + value=value, + display_type=None, + ) + attributes.append(attribute) + + return attributes + def parse_metadata(self, token: Token, *args, **kwargs) -> Metadata: # type: ignore[no-untyped-def] # noqa: E501 token.uri = f"https://api.wrappedpunks.com/api/punks/metadata/{token.token_id}" raw_data = self.fetcher.fetch_content(token.uri) @@ -105,3 +148,24 @@ def parse_metadata(self, token: Token, *args, **kwargs) -> Metadata: # type: ig image=image, additional_fields=self.parse_additional_fields(raw_data), # type: ignore[arg-type] # noqa: E501 ) + + async def _gen_parse_metadata_impl(self, token: Token, *args, **kwargs) -> Metadata: # type: ignore[no-untyped-def] # noqa: E501 + token.uri = f"https://api.wrappedpunks.com/api/punks/metadata/{token.token_id}" + raw_data, mime_and_size, image, attributes = await asyncio.gather( + self.fetcher.gen_fetch_content(token.uri), + self.fetcher.gen_fetch_mime_type_and_size(token.uri), + self.gen_image(token.token_id), + self.gen_parse_attributes(token.token_id), + ) + mime, _ = mime_and_size + + return Metadata( + token=token, + raw_data=raw_data, + attributes=attributes, + name=raw_data.get("name"), # type: ignore[union-attr] + description=raw_data.get("description"), # type: ignore[union-attr] + mime_type=mime, + image=image, + additional_fields=self.parse_additional_fields(raw_data), # type: ignore[arg-type] # noqa: E501 + ) diff --git a/offchain/metadata/parsers/collection/superrare.py b/offchain/metadata/parsers/collection/superrare.py index becd1b6..c9a81fe 100644 --- a/offchain/metadata/parsers/collection/superrare.py +++ b/offchain/metadata/parsers/collection/superrare.py @@ -1,3 +1,4 @@ +import asyncio from typing import Optional from offchain.constants.addresses import CollectionAddress @@ -29,6 +30,21 @@ def get_image_details(self, raw_data: dict) -> Optional[MediaDetails]: # type: pass return details + async def gen_image_details(self, raw_data: dict) -> Optional[MediaDetails]: # type: ignore[type-arg] # noqa: E501 + image_uri = raw_data.get("image") + if not image_uri: + return None + details = MediaDetails(uri=image_uri, size=None, sha256=None, mime_type=None) + try: + content_type, size = await self.fetcher.gen_fetch_mime_type_and_size( + image_uri + ) + details.mime_type = content_type + details.size = size + except Exception: + pass + return details + def get_content_details(self, raw_data: dict) -> Optional[MediaDetails]: # type: ignore[type-arg] # noqa: E501 media = raw_data.get("media") if not media or not isinstance(media, dict): @@ -52,6 +68,31 @@ def get_content_details(self, raw_data: dict) -> Optional[MediaDetails]: # type return details + async def gen_content_details(self, raw_data: dict) -> Optional[MediaDetails]: # type: ignore[type-arg] # noqa: E501 + media = raw_data.get("media") + if not media or not isinstance(media, dict): + return # type: ignore[return-value] + content_uri = media.get("uri") + if not content_uri: + return # type: ignore[return-value] + details = MediaDetails( + uri=content_uri, + size=media.get("size"), + sha256=None, + mime_type=media.get("mimeType"), + ) + if not details.mime_type: + try: + content_type, size = await self.fetcher.gen_fetch_mime_type_and_size( + content_uri + ) + details.mime_type = content_type + details.size = size + except Exception: + pass + + return details + def parse_additional_fields(self, raw_data: dict) -> list[MetadataField]: # type: ignore[type-arg] # noqa: E501 additional_fields = [] if created_by := raw_data.get("createdBy"): @@ -84,7 +125,6 @@ def parse_additional_fields(self, raw_data: dict) -> list[MetadataField]: # typ return additional_fields def parse_metadata(self, token: Token, raw_data: Optional[dict], *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 - mime_type, _ = self.fetcher.fetch_mime_type_and_size(token.uri) # type: ignore[arg-type] # noqa: E501 return Metadata( @@ -98,3 +138,23 @@ def parse_metadata(self, token: Token, raw_data: Optional[dict], *args, **kwargs additional_fields=self.parse_additional_fields(raw_data), # type: ignore[arg-type] # noqa: E501 attributes=[], ) + + async def _gen_parse_metadata_impl(self, token: Token, raw_data: Optional[dict], *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 + mime_and_size, image, content = await asyncio.gather( + self.fetcher.gen_fetch_mime_type_and_size(token.uri), # type: ignore[arg-type] # noqa: E501 + self.gen_image_details(raw_data), # type: ignore[arg-type] # noqa: E501 + self.gen_content_details(raw_data), # type: ignore[arg-type] # noqa: E501 + ) + mime_type, _ = mime_and_size + + return Metadata( + token=token, + raw_data=raw_data, + name=raw_data.get("name"), # type: ignore[union-attr] + description=raw_data.get("description"), # type: ignore[union-attr] + mime_type=mime_type, + image=image, # type: ignore[arg-type] + content=content, # type: ignore[arg-type] + additional_fields=self.parse_additional_fields(raw_data), # type: ignore[arg-type] # noqa: E501 + attributes=[], + ) diff --git a/offchain/metadata/parsers/collection/zora.py b/offchain/metadata/parsers/collection/zora.py index 761e12d..b4d08da 100644 --- a/offchain/metadata/parsers/collection/zora.py +++ b/offchain/metadata/parsers/collection/zora.py @@ -1,9 +1,10 @@ +import asyncio from typing import Optional from offchain.constants.addresses import CollectionAddress from offchain.metadata.models.metadata import ( - Metadata, MediaDetails, + Metadata, MetadataField, MetadataFieldType, ) @@ -46,6 +47,19 @@ def get_uri(self, token_id: int) -> Optional[str]: return results[0] + async def gen_uri(self, token_id: int) -> Optional[str]: + results = await self.contract_caller.rpc.async_reader.gen_call_single_function_single_address_many_args( + ADDRESS, + function_sig="tokenMetadataURI(uint256)", + return_type=["string"], + args=[[token_id]], + ) + + if len(results) < 1: + return None + + return results[0] + def get_content_uri(self, token_id: int) -> Optional[str]: results = self.contract_caller.single_address_single_fn_many_args( ADDRESS, @@ -59,6 +73,19 @@ def get_content_uri(self, token_id: int) -> Optional[str]: return results[0] + async def gen_content_uri(self, token_id: int) -> Optional[str]: + results = await self.contract_caller.rpc.async_reader.gen_call_single_function_single_address_many_args( + ADDRESS, + function_sig="tokenURI(uint256)", + return_type=["string"], + args=[[token_id]], + ) + + if len(results) < 1: + return None + + return results[0] + def get_content_details(self, uri: str) -> Optional[MediaDetails]: try: content_type, size = self.fetcher.fetch_mime_type_and_size(uri) @@ -68,6 +95,15 @@ def get_content_details(self, uri: str) -> Optional[MediaDetails]: return None + async def gen_content_details(self, uri: str) -> Optional[MediaDetails]: + try: + content_type, size = await self.fetcher.gen_fetch_mime_type_and_size(uri) + return MediaDetails(uri=uri, size=size, sha256=None, mime_type=content_type) + except Exception: + pass + + return None + def parse_metadata(self, token: Token, raw_data: Optional[dict], *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 if token.uri is None or raw_data is None or not isinstance(raw_data, dict): token.uri = self.get_uri(token.token_id) @@ -89,3 +125,29 @@ def parse_metadata(self, token: Token, raw_data: Optional[dict], *args, **kwargs metadata.mime_type = "application/json" # type: ignore[union-attr] return metadata + + async def _gen_parse_metadata_impl(self, token: Token, raw_data: Optional[dict], *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 + if token.uri is None: + token.uri = await self.gen_uri(token.token_id) + if raw_data is None or not isinstance(raw_data, dict): + raw_data = await self.fetcher.gen_fetch_content( # type:ignore[assignment] + token.uri # type:ignore[arg-type] + ) + + metadata, content_uri = await asyncio.gather( + DefaultCatchallParser(self.fetcher).gen_parse_metadata(token=token, raw_data=raw_data), # type: ignore[arg-type] # noqa: E501 + self.gen_content_uri(token.token_id), + ) + content = await self.gen_content_details(content_uri) # type: ignore[arg-type] + + # if we have an image, make sure we set + # the image field, otherwise fallback to content + if content.mime_type.startswith("image"): # type: ignore[union-attr] + metadata.image = content # type: ignore[union-attr] + else: + metadata.content = content # type: ignore[union-attr] + + metadata.additional_fields = self.parse_additional_fields(raw_data) # type: ignore[arg-type, union-attr] # noqa: E501 + metadata.mime_type = "application/json" # type: ignore[union-attr] + + return metadata diff --git a/offchain/metadata/parsers/schema/opensea.py b/offchain/metadata/parsers/schema/opensea.py index bffaa0b..5a207ce 100644 --- a/offchain/metadata/parsers/schema/opensea.py +++ b/offchain/metadata/parsers/schema/opensea.py @@ -121,6 +121,61 @@ def parse_metadata(self, token: Token, raw_data: dict, *args, **kwargs) -> Optio additional_fields=self.parse_additional_fields(raw_data), ) + async def _gen_parse_metadata_impl(self, token: Token, raw_data: dict, *args, **kwargs) -> Optional[Metadata]: # type: ignore[no-untyped-def, type-arg] # noqa: E501 + """Given a token and raw data returned from the token uri, return a normalized Metadata object. + + Args: + token (Token): token to process metadata for. + raw_data (dict): raw data returned from token uri. + + Returns: + Optional[Metadata]: normalized metadata object, if successfully parsed. + """ # noqa: E501 + mime, _ = await self.fetcher.gen_fetch_mime_type_and_size(token.uri) # type: ignore[arg-type] # noqa: E501 + + attributes = [ + self.parse_attribute(attribute) + for attribute in raw_data.get("attributes", []) + ] # noqa: E501 + image = None + image_uri = raw_data.get("image") or raw_data.get("image_data") + if image_uri: + image_mime, image_size = await self.fetcher.gen_fetch_mime_type_and_size( + image_uri + ) + image = MediaDetails(size=image_size, uri=image_uri, mime_type=image_mime) + + content = None + content_uri = raw_data.get("animation_url") + if content_uri: + ( + content_mime, + content_size, + ) = await self.fetcher.gen_fetch_mime_type_and_size( + content_uri + ) # noqa: E501 + content = MediaDetails( + uri=content_uri, size=content_size, mime_type=content_mime + ) # noqa: E501 + + if image and image.mime_type: + mime = image.mime_type + + if content and content.mime_type: + mime = content.mime_type + + return Metadata( + token=token, + raw_data=raw_data, + attributes=attributes, + name=raw_data.get("name"), + description=raw_data.get("description"), + mime_type=mime, + image=image, + content=content, + additional_fields=self.parse_additional_fields(raw_data), + ) + def should_parse_token(self, token: Token, raw_data: Optional[dict], *args, **kwargs) -> bool: # type: ignore[no-untyped-def, type-arg] # noqa: E501 """Return whether or not a collection parser should parse a given token. diff --git a/offchain/metadata/pipelines/metadata_pipeline.py b/offchain/metadata/pipelines/metadata_pipeline.py index 542a97f..f7e9598 100644 --- a/offchain/metadata/pipelines/metadata_pipeline.py +++ b/offchain/metadata/pipelines/metadata_pipeline.py @@ -131,6 +131,27 @@ def fetch_token_uri( ) return res[0] if res and len(res) > 0 else None + async def gen_fetch_token_uri( + self, token: Token, function_signature: str = "tokenURI(uint256)" + ) -> Optional[str]: + """Given a token, fetch the token uri from the contract using a specified function signature. + + Args: + token (Token): token whose uri we want to fetch. + function_signature (str, optional): token uri contract function signature. Defaults to "tokenURI(uint256)". + + Returns: + Optional[str]: the token uri, if found. + """ # noqa: E501 + + res = await self.contract_caller.rpc.async_reader.gen_call_single_function_single_address_many_args( + address=token.collection_address, + function_sig=function_signature, + return_type=["string"], + args=[[token.token_id]], + ) + return res[0] if res and len(res) > 0 else None + def fetch_token_metadata( self, token: Token, @@ -208,7 +229,7 @@ def fetch_token_metadata( return metadata_selector_fn(possible_metadatas_or_errors) # type: ignore[no-any-return] # noqa: E501 return possible_metadatas_or_errors[0] - async def async_fetch_token_metadata( + async def gen_fetch_token_metadata( self, token: Token, metadata_selector_fn: Optional[Callable] = None, # type: ignore[type-arg] @@ -225,39 +246,33 @@ async def async_fetch_token_metadata( Union[Metadata, MetadataProcessingError]: returns either a Metadata or a MetadataProcessingError if unable to parse. """ - possible_metadatas_or_errors = [] + possible_metadatas_or_errors: list[ + Union[Metadata, MetadataProcessingError] + ] = [] - # If no token uri is passed in, try to fetch the token uri from the contract - if token.uri is None: - try: - token.uri = self.fetch_token_uri(token) - except Exception as e: - error_message = f"({token.chain_identifier}-{token.collection_address}-{token.token_id}) Failed to fetch token uri. {str(e)}" # noqa: E501 - logger.error(error_message) - possible_metadatas_or_errors.append( - MetadataProcessingError.from_token_and_error( - token=token, e=Exception(error_message) - ) - ) + if not token.uri: + return MetadataProcessingError.from_token_and_error( + token=token, e=Exception("Token has not uri") + ) raw_data = None - # Try to fetch the raw data from the token uri - if token.uri is not None: - try: - raw_data = await self.fetcher.gen_fetch_content(token.uri) - except Exception as e: - error_message = f"({token.chain_identifier}-{token.collection_address}-{token.token_id}) Failed to parse token uri: {token.uri}. {str(e)}" # noqa: E501 - logger.error(error_message) - possible_metadatas_or_errors.append( - MetadataProcessingError.from_token_and_error( - token=token, e=Exception(error_message) - ) + try: + raw_data = await self.fetcher.gen_fetch_content(token.uri) + except Exception as e: + error_message = f"({token.chain_identifier}-{token.collection_address}-{token.token_id}) Failed to parse token uri: {token.uri}. {str(e)}" # noqa: E501 + logger.error(error_message) + possible_metadatas_or_errors.append( + MetadataProcessingError.from_token_and_error( + token=token, e=Exception(error_message) ) + ) - for parser in self.parsers: + async def gen_parse_metadata( + parser: BaseParser, + ) -> Optional[Union[Metadata, MetadataProcessingError]]: if not parser.should_parse_token(token=token, raw_data=raw_data): # type: ignore[arg-type] # noqa: E501 - continue + return None try: metadata_or_error = parser.parse_metadata( token=token, raw_data=raw_data # type: ignore[arg-type] @@ -270,7 +285,16 @@ async def async_fetch_token_metadata( metadata_or_error = MetadataProcessingError.from_token_and_error( # type: ignore[assignment] # noqa: E501 token=token, e=e ) - possible_metadatas_or_errors.append(metadata_or_error) # type: ignore[arg-type] # noqa: E501 + return metadata_or_error + + nullable_possible_metadatas_or_errors: list[ + Optional[Union[Metadata, MetadataProcessingError]] + ] = await asyncio.gather( + *(gen_parse_metadata(parser) for parser in self.parsers) + ) + possible_metadatas_or_errors += filter( + None, nullable_possible_metadatas_or_errors + ) if len(possible_metadatas_or_errors) == 0: possible_metadatas_or_errors.append( MetadataProcessingError.from_token_and_error( @@ -341,8 +365,7 @@ async def async_run( # type: ignore[no-untyped-def] if len(tokens) == 0: return [] tasks = [ - self.async_fetch_token_metadata(token, select_metadata_fn) - for token in tokens + self.gen_fetch_token_metadata(token, select_metadata_fn) for token in tokens ] metadatas_or_errors = await asyncio.gather(*tasks) diff --git a/offchain/web3/read_async.py b/offchain/web3/read_async.py index aecfff2..e35d4cc 100644 --- a/offchain/web3/read_async.py +++ b/offchain/web3/read_async.py @@ -9,7 +9,6 @@ from web3 import Web3 from web3.eth import AsyncEth -from offchain.utils.utils import safe_async_runner from offchain.web3.contract_utils import function_signature_to_sighash @@ -194,7 +193,6 @@ async def get_code(self, contract_address: str) -> Optional[Any]: params=[contract_address], ) - @safe_async_runner(attempt=3, timeout=2, silent=True) async def _request( self, method: str, diff --git a/tests/metadata/parsers/test_opensea_parser.py b/tests/metadata/parsers/test_opensea_parser.py index cadd714..6fe4b55 100644 --- a/tests/metadata/parsers/test_opensea_parser.py +++ b/tests/metadata/parsers/test_opensea_parser.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock +import pytest + from offchain.metadata.adapters.ipfs import IPFSAdapter from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher from offchain.metadata.models.metadata import ( @@ -218,3 +220,12 @@ def test_opensea_parser_parses_metadata_with_content(self): # type: ignore[no-u ), ], ) + + @pytest.mark.asyncio + async def test_opensea_parser_gen_parses_metadata(self, raw_crypto_coven_metadata): # type: ignore[no-untyped-def] + fetcher = MetadataFetcher() + parser = OpenseaParser(fetcher=fetcher) # type: ignore[abstract] + metadata = await parser.gen_parse_metadata( + token=self.token, raw_data=raw_crypto_coven_metadata + ) + assert metadata diff --git a/tests/metadata/parsers/test_punks_parser.py b/tests/metadata/parsers/test_punks_parser.py index de7965e..d6b0296 100644 --- a/tests/metadata/parsers/test_punks_parser.py +++ b/tests/metadata/parsers/test_punks_parser.py @@ -1,13 +1,15 @@ # flake8: noqa: E501 from unittest.mock import MagicMock +import pytest + from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher from offchain.metadata.models.metadata import ( + Attribute, MediaDetails, Metadata, MetadataField, MetadataFieldType, - Attribute, ) from offchain.metadata.models.token import Token from offchain.metadata.parsers.collection.punks import PunksParser @@ -89,3 +91,13 @@ def test_punks_parser_parses_metadata(self): # type: ignore[no-untyped-def] ), ], ) + + @pytest.mark.asyncio + async def test_punks_parser_gen_parses_metadata(self): # type: ignore[no-untyped-def] + fetcher = MetadataFetcher() + contract_caller = ContractCaller() + parser = PunksParser(fetcher=fetcher, contract_caller=contract_caller) # type: ignore[abstract] + metadata = await parser.gen_parse_metadata( + token=self.token, raw_data=self.raw_data + ) + assert metadata diff --git a/tests/metadata/parsers/test_superrare_parser.py b/tests/metadata/parsers/test_superrare_parser.py index 7f1aa57..0f74ffa 100644 --- a/tests/metadata/parsers/test_superrare_parser.py +++ b/tests/metadata/parsers/test_superrare_parser.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock +import pytest + from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher from offchain.metadata.models.metadata import ( MediaDetails, @@ -119,3 +121,13 @@ def test_superrare_parser_parses_metadata(self): # type: ignore[no-untyped-def] ), ], ) + + @pytest.mark.asyncio + async def test_superrare_parser_gen_parses_metadata(self): # type: ignore[no-untyped-def] + fetcher = MetadataFetcher() + contract_caller = ContractCaller() + parser = SuperRareParser(fetcher=fetcher, contract_caller=contract_caller) # type: ignore[abstract] + metadata = await parser.gen_parse_metadata( + token=self.token, raw_data=self.raw_data + ) + assert metadata diff --git a/tests/metadata/parsers/test_zora_parser.py b/tests/metadata/parsers/test_zora_parser.py index 616920f..3dd7ae8 100644 --- a/tests/metadata/parsers/test_zora_parser.py +++ b/tests/metadata/parsers/test_zora_parser.py @@ -1,5 +1,7 @@ # flake8: noqa: E501 -from unittest.mock import Mock, MagicMock +from unittest.mock import MagicMock, Mock + +import pytest from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher from offchain.metadata.models.metadata import ( @@ -74,3 +76,46 @@ def test_zora_parser_parses_metadata(self): # type: ignore[no-untyped-def] ) ], ) + + @pytest.mark.asyncio + async def test_zora_parser_gen_parses_metadata(self): # type: ignore[no-untyped-def] + fetcher = MetadataFetcher() + contract_caller = ContractCaller() + parser = ZoraParser(fetcher=fetcher, contract_caller=contract_caller) # type: ignore[abstract] + metadata = await parser.gen_parse_metadata( + token=self.token, raw_data=self.raw_data + ) + assert metadata == Metadata( + token=Token( + collection_address="0xabefbc9fd2f806065b4f3c237d4b59d9a97bcac7", + token_id=5769, + chain_identifier="ETHEREUM-MAINNET", + uri="https://zora-dev.mypinata.cloud/ipfs/bafkreigux6jujn5hvlmptgzgok4reaie2gkuvsk2kynnalsyfgr4g35dkm", + ), + raw_data={ + "description": "A Lonely Soul,\n\nI've felt lonely lately. Somewhere deep inside, detached. \n\nThere must be plenty of lost souls wandering the globe. Looking to belong; to understand their purpose.\n\nI know my purpose, but I fear I've burned up surviving to the moment.\n\nDoes this count?\nAm I still pushing forward?\n\nI hope so...\n\nDo I still have time?\nAm I just floating?\n\nPlease, don't give up.\n\nAge 23 (2021)\n4096x4096px", + "mimeType": "image/jpeg", + "name": "Reform: A Lonely Soul", + "version": "zora-20210101", + }, + attributes=[], + standard=None, + name="Reform: A Lonely Soul", + description="A Lonely Soul,\n\nI've felt lonely lately. Somewhere deep inside, detached. \n\nThere must be plenty of lost souls wandering the globe. Looking to belong; to understand their purpose.\n\nI know my purpose, but I fear I've burned up surviving to the moment.\n\nDoes this count?\nAm I still pushing forward?\n\nI hope so...\n\nDo I still have time?\nAm I just floating?\n\nPlease, don't give up.\n\nAge 23 (2021)\n4096x4096px", + mime_type="application/json", + image=MediaDetails( + size=13548199, + sha256=None, + uri="https://zora-dev.mypinata.cloud/ipfs/bafybeiffwxjez2axebcprj2h7wkohr2pdbvuv37f7uxyptuw7o6t5fvppu", + mime_type="image/jpeg", + ), + content=None, + additional_fields=[ + MetadataField( + field_name="version", + type=MetadataFieldType.TEXT, + description="Zora Metadata version", + value="zora-20210101", + ) + ], + )