From b2db0bdabef6cb865527fbcb2454d7de285572c5 Mon Sep 17 00:00:00 2001 From: will0x0909 <166356797+will0x0909@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:07:09 +0800 Subject: [PATCH 01/12] eigen_layer support (#166) * add eigen_layer --- enumeration/entity_type.py | 7 + indexer/domain/transaction.py | 3 + .../models/address_contract_operation.py | 2 +- .../models/address_internal_transaciton.py | 2 +- .../modules/custom/eigen_layer/__init__.py | 6 + .../custom/eigen_layer/eigen_layer_abi.py | 64 ++++ .../custom/eigen_layer/eigen_layer_conf.py | 27 ++ .../custom/eigen_layer/eigen_layer_domain.py | 47 +++ .../custom/eigen_layer/exportEigenLayerJob.py | 320 ++++++++++++++++++ .../custom/eigen_layer/models/__init__.py | 6 + .../models/af_eigen_layer_address_current.py | 43 +++ .../models/af_eigen_layer_records.py | 57 ++++ 12 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 indexer/modules/custom/eigen_layer/__init__.py create mode 100644 indexer/modules/custom/eigen_layer/eigen_layer_abi.py create mode 100644 indexer/modules/custom/eigen_layer/eigen_layer_conf.py create mode 100644 indexer/modules/custom/eigen_layer/eigen_layer_domain.py create mode 100644 indexer/modules/custom/eigen_layer/exportEigenLayerJob.py create mode 100644 indexer/modules/custom/eigen_layer/models/__init__.py create mode 100644 indexer/modules/custom/eigen_layer/models/af_eigen_layer_address_current.py create mode 100644 indexer/modules/custom/eigen_layer/models/af_eigen_layer_records.py diff --git a/enumeration/entity_type.py b/enumeration/entity_type.py index 79cf45e83..fd6f28983 100644 --- a/enumeration/entity_type.py +++ b/enumeration/entity_type.py @@ -21,6 +21,7 @@ from indexer.modules.custom.blue_chip.domain.feature_blue_chip import BlueChipHolder from indexer.modules.custom.deposit_to_l2.domain.address_token_deposit import AddressTokenDeposit from indexer.modules.custom.deposit_to_l2.domain.token_deposit_transaction import TokenDepositTransaction +from indexer.modules.custom.eigen_layer.eigen_layer_domain import EigenLayerActionD, EigenLayerAddressCurrentD from indexer.modules.custom.hemera_ens.ens_domain import ( ENSAddressChangeD, ENSAddressD, @@ -65,6 +66,8 @@ class EntityType(IntFlag): ENS = 1 << 10 + EIGEN_LAYER = 1 << 13 + EXPLORER = EXPLORER_BASE | EXPLORER_TOKEN | EXPLORER_TRACE @staticmethod @@ -182,3 +185,7 @@ def generate_output_types(entity_types): if entity_types & EntityType.OPEN_SEA: yield AddressOpenseaTransaction yield OpenseaOrder + + if entity_types & EntityType.EIGEN_LAYER: + yield EigenLayerActionD + yield EigenLayerAddressCurrentD diff --git a/indexer/domain/transaction.py b/indexer/domain/transaction.py index c3deae306..0ea5febba 100644 --- a/indexer/domain/transaction.py +++ b/indexer/domain/transaction.py @@ -66,3 +66,6 @@ def fill_with_receipt(self, receipt: Receipt): self.receipt = receipt if self.to_address is None: self.to_address = self.receipt.contract_address + + def get_method_id(self): + return self.input[0:10] if self.input and len(self.input) > 10 else None diff --git a/indexer/modules/custom/address_index/models/address_contract_operation.py b/indexer/modules/custom/address_index/models/address_contract_operation.py index 91f26f39b..70b9fc423 100644 --- a/indexer/modules/custom/address_index/models/address_contract_operation.py +++ b/indexer/modules/custom/address_index/models/address_contract_operation.py @@ -43,7 +43,7 @@ def model_domain_mapping(): Index( - "address_contract_operations_address_block_timestamp_block_number_t_idx", + "address_contract_operations_address_block_tn_t_idx", AddressContractOperations.address, desc(AddressContractOperations.block_timestamp), desc(AddressContractOperations.block_number), diff --git a/indexer/modules/custom/address_index/models/address_internal_transaciton.py b/indexer/modules/custom/address_index/models/address_internal_transaciton.py index a2e97735a..dac432eed 100644 --- a/indexer/modules/custom/address_index/models/address_internal_transaciton.py +++ b/indexer/modules/custom/address_index/models/address_internal_transaciton.py @@ -43,7 +43,7 @@ def model_domain_mapping(): Index( - "address_internal_transactions_address_block_timestamp_block_number_t_idx", + "address_internal_transactions_address_nt_t_idx", AddressInternalTransactions.address, desc(AddressInternalTransactions.block_timestamp), desc(AddressInternalTransactions.block_number), diff --git a/indexer/modules/custom/eigen_layer/__init__.py b/indexer/modules/custom/eigen_layer/__init__.py new file mode 100644 index 000000000..e14305a10 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 17:12 +# @Author will +# @File __init__.py.py +# @Brief diff --git a/indexer/modules/custom/eigen_layer/eigen_layer_abi.py b/indexer/modules/custom/eigen_layer/eigen_layer_abi.py new file mode 100644 index 000000000..d881d19d6 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/eigen_layer_abi.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:37 +# @Author will +# @File eigen_layer_abi.py +# @Brief +import json +from typing import cast + +from web3.types import ABIEvent, ABIFunction + +from indexer.utils.abi import event_log_abi_to_topic, function_abi_to_4byte_selector_str + +DEPOSIT_EVENT = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"contract IERC20","name":"token","type":"address"},{"indexed":false,"internalType":"contract IStrategy","name":"strategy","type":"address"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Deposit","type":"event"} +""" + ), +) +DEPOSIT_EVENT_SIG = event_log_abi_to_topic(DEPOSIT_EVENT) + +WITHDRAWAL_QUEUED_EVENT = cast( + ABIEvent, + json.loads( + """ + {"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"withdrawalRoot","type":"bytes32"},{"components":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"address","name":"delegatedTo","type":"address"},{"internalType":"address","name":"withdrawer","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint32","name":"startBlock","type":"uint32"},{"internalType":"contract IStrategy[]","name":"strategies","type":"address[]"},{"internalType":"uint256[]","name":"shares","type":"uint256[]"}],"indexed":false,"internalType":"struct Withdrawal","name":"withdrawal","type":"tuple"}],"name":"WithdrawalQueued","type":"event"} +""" + ), +) +WITHDRAWAL_QUEUED_EVENT_SIG = event_log_abi_to_topic(WITHDRAWAL_QUEUED_EVENT) + +WITHDRAWAL_QUEUED_EVENT_2 = cast( + ABIEvent, + json.loads( + """{"type":"event","name":"WithdrawalQueued","inputs":[{"type":"address","name":"depositor","indexed":false},{"type":"uint96","name":"nonce","indexed":false},{"type":"address","name":"withdrawer","indexed":false},{"type":"address","name":"delegatedAddress","indexed":false},{"type":"bytes32","name":"withdrawalRoot","indexed":false}],"anonymous":false} +""" + ), +) + +SHARE_WITHDRAW_QUEUED = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":false,"name":"depositor","type":"address"},{"indexed":false,"name":"nonce","type":"uint96"},{"indexed":false,"name":"strategy","type":"address"},{"indexed":false,"name":"shares","type":"uint256"}],"name":"ShareWithdrawalQueued","type":"event"}""" + ), +) + +WITHDRAWAL_COMPLETED_EVENT = cast( + ABIEvent, + json.loads( + """{"type":"event","name":"WithdrawalCompleted","inputs":[{"type":"bytes32","name":"withdrawalRoot","indexed":false}],"anonymous":false} + """ + ), +) +WITHDRAWAL_COMPLETED_EVENT_SIG = event_log_abi_to_topic(WITHDRAWAL_COMPLETED_EVENT) + + +FINISH_WITHDRAWAL_FUNCTION = cast( + ABIFunction, + json.loads( + """{"inputs":[{"components":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"address","name":"delegatedTo","type":"address"},{"internalType":"address","name":"withdrawer","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint32","name":"startBlock","type":"uint32"},{"internalType":"address[]","name":"strategies","type":"address[]"},{"internalType":"uint256[]","name":"shares","type":"uint256[]"}],"internalType":"struct Withdrawal[]","name":"withdrawals","type":"tuple[]"},{"internalType":"address[][]","name":"tokens","type":"address[][]"},{"internalType":"uint256[]","name":"middlewareTimesIndexes","type":"uint256[]"},{"internalType":"bool[]","name":"receiveAsTokens","type":"bool[]"}],"name":"completeQueuedWithdrawals","outputs":[],"stateMutability":"nonpayable","type":"function"}""" + ), +) +FINISH_WITHDRAWAL_FUNCTION_4SIG = function_abi_to_4byte_selector_str(FINISH_WITHDRAWAL_FUNCTION) diff --git a/indexer/modules/custom/eigen_layer/eigen_layer_conf.py b/indexer/modules/custom/eigen_layer/eigen_layer_conf.py new file mode 100644 index 000000000..1a4f790d4 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/eigen_layer_conf.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:37 +# @Author will +# @File eigen_layer_conf.py +# @Brief +CHAIN_CONTRACT = { + 1: { + "DEPOSIT": { + "address": "0x858646372cc42e1a627fce94aa7a7033e7cf075a", + "topic": "0x7cfff908a4b583f36430b25d75964c458d8ede8a99bd61be750e97ee1b2f3a96", + }, + "START_WITHDRAW": { + "address": "0x39053d51b77dc0d36036fc1fcc8cb819df8ef37a", + "topic": "0x9009ab153e8014fbfb02f2217f5cde7aa7f9ad734ae85ca3ee3f4ca2fdd499f9", + }, + "START_WITHDRAW_2": { + "address": "0x858646372cc42e1a627fce94aa7a7033e7cf075a", + "topic": "0x32cf9fc97155f52860a59a99879a2e89c1e53f28126a9ab6a2ff29344299e674", + "prev_topic": "0xcf1c2370141bbd0a6d971beb0e3a2455f24d6e773ddc20ccc1c4e32f3dd9f9f7", + }, + "FINISH_WITHDRAW": { + "address": "0x39053d51b77dc0d36036fc1fcc8cb819df8ef37a", + "topic": "0xc97098c2f658800b4df29001527f7324bcdffcf6e8751a699ab920a1eced5b1d", + }, + } +} diff --git a/indexer/modules/custom/eigen_layer/eigen_layer_domain.py b/indexer/modules/custom/eigen_layer/eigen_layer_domain.py new file mode 100644 index 000000000..80bebc711 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/eigen_layer_domain.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from typing import Optional + +from indexer.domain import FilterData + + +@dataclass +class EigenLayerActionD(FilterData): + transaction_hash: str + log_index: int + transaction_index: int + internal_idx: Optional[int] = 0 + block_number: Optional[int] = None + block_timestamp: Optional[int] = None + method: Optional[str] = None + event_name: Optional[str] = None + topic0: Optional[str] = None + from_address: Optional[str] = None + to_address: Optional[str] = None + + token: Optional[str] = None + strategy: Optional[str] = None + shares: Optional[int] = None + staker: Optional[str] = None + withdrawer: Optional[str] = None + withdrawroot: Optional[str] = None + + +@dataclass +class EigenLayerAddressCurrentD(FilterData): + address: Optional[str] = None + strategy: Optional[str] = None + token: Optional[str] = None + deposit_amount: Optional[int] = None + start_withdraw_amount: Optional[int] = None + finish_withdraw_amount: Optional[int] = None + + +def eigen_layer_address_current_factory(): + return EigenLayerAddressCurrentD( + address=None, + strategy=None, + token=None, + deposit_amount=0, + start_withdraw_amount=0, + finish_withdraw_amount=0, + ) diff --git a/indexer/modules/custom/eigen_layer/exportEigenLayerJob.py b/indexer/modules/custom/eigen_layer/exportEigenLayerJob.py new file mode 100644 index 000000000..579477280 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/exportEigenLayerJob.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:07 +# @Author will +# @File exportEigenLayerJob.py +# @Brief +import logging +from collections import defaultdict +from typing import Any, Dict, List + +from eth_abi import decode +from eth_typing import Decodable +from sqlalchemy import func + +from common.utils.exception_control import FastShutdownError +from indexer.domain.transaction import Transaction +from indexer.executors.batch_work_executor import BatchWorkExecutor +from indexer.jobs import FilterTransactionDataJob +from indexer.modules.custom.eigen_layer.eigen_layer_abi import ( + DEPOSIT_EVENT, + SHARE_WITHDRAW_QUEUED, + WITHDRAWAL_COMPLETED_EVENT, + WITHDRAWAL_QUEUED_EVENT, + WITHDRAWAL_QUEUED_EVENT_2, +) +from indexer.modules.custom.eigen_layer.eigen_layer_conf import CHAIN_CONTRACT +from indexer.modules.custom.eigen_layer.eigen_layer_domain import ( + EigenLayerActionD, + EigenLayerAddressCurrentD, + eigen_layer_address_current_factory, +) +from indexer.modules.custom.eigen_layer.models.af_eigen_layer_address_current import AfEigenLayerAddressCurrent +from indexer.modules.custom.eigen_layer.models.af_eigen_layer_records import AfEigenLayerRecords +from indexer.specification.specification import TopicSpecification, TransactionFilterByLogs +from indexer.utils.abi import bytes_to_hex_str, decode_log + +logger = logging.getLogger(__name__) + + +class ExportEigenLayerJob(FilterTransactionDataJob): + dependency_types = [Transaction] + output_types = [EigenLayerActionD, EigenLayerAddressCurrentD] + able_to_reorg = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self._batch_work_executor = BatchWorkExecutor( + kwargs["batch_size"], + kwargs["max_workers"], + job_name=self.__class__.__name__, + ) + + self._is_batch = kwargs["batch_size"] > 1 + self.db_service = kwargs["config"].get("db_service") + self.chain_id = self._web3.eth.chain_id + self.eigen_layer_conf = CHAIN_CONTRACT[self.chain_id] + + def get_filter(self): + # deposit, startWithdraw, finishWithdraw + topics = [] + addresses = [] + for k, item in self.eigen_layer_conf.items(): + topics.append(item["topic"]) + addresses.append(item["address"]) + + return [ + TransactionFilterByLogs(topics_filters=[TopicSpecification(topics=topics, addresses=addresses)]), + ] + + def _collect(self, **kwargs): + transactions: List[Transaction] = self._data_buff.get(Transaction.type(), []) + res = [] + + for transaction in transactions: + logs = transaction.receipt.logs + for log in logs: + if ( + log.topic0 == self.eigen_layer_conf["DEPOSIT"]["topic"] + and log.address == self.eigen_layer_conf["DEPOSIT"]["address"] + ): + dl = decode_log(DEPOSIT_EVENT, log) + staker = dl.get("staker") + token = dl.get("token") + strategy = dl.get("strategy") + shares = dl.get("shares") + + kad = EigenLayerActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=DEPOSIT_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + strategy=strategy, + token=token, + shares=shares, + staker=staker, + ) + res.append(kad) + elif ( + log.topic0 == self.eigen_layer_conf["START_WITHDRAW"]["topic"] + and log.address == self.eigen_layer_conf["START_WITHDRAW"]["address"] + ): + dl = decode_log(WITHDRAWAL_QUEUED_EVENT, log) + + withdrawal_root = dl.get("withdrawalRoot") + withdrawal_struct = dl.get("withdrawal") + + staker = withdrawal_struct.get("staker") + withdrawer = withdrawal_struct.get("withdrawer") + shares_lis = withdrawal_struct.get("shares") + strategy_lis = withdrawal_struct.get("strategies") + if len(shares_lis) != len(strategy_lis): + raise FastShutdownError(f"eigen_layer_job error data tnx {transaction.hash}") + for idx in range(len(strategy_lis)): + strategy = strategy_lis[idx] + shares = shares_lis[idx] + kad = EigenLayerActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + internal_idx=idx, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=WITHDRAWAL_QUEUED_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + strategy=strategy, + staker=staker, + withdrawer=withdrawer, + shares=shares, + withdrawroot=withdrawal_root, + ) + res.append(kad) + elif ( + log.topic0 == self.eigen_layer_conf["START_WITHDRAW_2"]["topic"] + and log.address == self.eigen_layer_conf["START_WITHDRAW_2"]["address"] + ): + dl = decode_log(WITHDRAWAL_QUEUED_EVENT_2, log) + + withdrawal_root = dl.get("withdrawalRoot") + + staker = dl.get("depositor") + withdrawer = dl.get("withdrawer") + nonce = dl.get("nonce") + internal_idx = 0 + for lg in logs: + if lg.topic0 == self.eigen_layer_conf["START_WITHDRAW_2"]["prev_topic"]: + dl2 = decode_log(SHARE_WITHDRAW_QUEUED, lg) + dl2_nonce = dl2.get("nonce") + if dl2_nonce == nonce: + shares = dl2.get("shares") + strategy = dl2.get("strategy") + + kad = EigenLayerActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + internal_idx=internal_idx, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=WITHDRAWAL_QUEUED_EVENT_2["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + strategy=strategy, + staker=staker, + withdrawer=withdrawer, + shares=shares, + withdrawroot=withdrawal_root, + ) + internal_idx += 1 + res.append(kad) + elif ( + log.topic0 == self.eigen_layer_conf["FINISH_WITHDRAW"]["topic"] + and log.address == self.eigen_layer_conf["FINISH_WITHDRAW"]["address"] + ): + dl = decode_log(WITHDRAWAL_COMPLETED_EVENT, log) + withdrawal_root = dl.get("withdrawalRoot") + res.append( + EigenLayerActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + transaction_index=transaction.transaction_index, + internal_idx=0, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=WITHDRAWAL_COMPLETED_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + staker=None, + withdrawer=None, + shares=None, + strategy=None, + token=None, + withdrawroot=withdrawal_root, + ) + ) + self.enrich_complete_withdraw(res) + for item in res: + self._collect_item(item.type(), item) + batch_result_dic = self.calculate_batch_result(res) + exists_dic = self.get_existing_address_current(list(batch_result_dic.keys())) + for address, outer_dic in batch_result_dic.items(): + for vault, kad in outer_dic.items(): + if address in exists_dic and vault in exists_dic[address]: + exists_kad = exists_dic[address][vault] + exists_kad.deposit_amount += kad.deposit_amount + exists_kad.start_withdraw_amount += kad.start_withdraw_amount + exists_kad.finish_withdraw_amount += kad.finish_withdraw_amount + self._collect_item(kad.type(), exists_kad) + else: + self._collect_item(kad.type(), kad) + + @staticmethod + def decode_function(decode_types, output: Decodable) -> Any: + try: + return decode(decode_types, output) + except Exception as e: + logger.error(e) + return [None] * len(decode_types) + + def get_existing_address_current(self, addresses): + if not self.db_service: + return {} + + addresses = [ad[2:] for ad in addresses if ad and ad.startswith("0x")] + if not addresses: + return {} + with self.db_service.get_service_session() as session: + query = session.query(AfEigenLayerAddressCurrent).filter( + func.encode(AfEigenLayerAddressCurrent.address, "hex").in_(addresses) + ) + result = query.all() + lis = [] + for rr in result: + lis.append( + EigenLayerAddressCurrentD( + address=bytes_to_hex_str(rr.address), + strategy=bytes_to_hex_str(rr.strategy), + token=bytes_to_hex_str(rr.token) if rr.token else None, + deposit_amount=rr.deposit_amount, + start_withdraw_amount=rr.start_withdraw_amount, + finish_withdraw_amount=rr.finish_withdraw_amount, + ) + ) + + return create_nested_dict(lis) + + def enrich_complete_withdraw(self, actions: List[EigenLayerActionD]): + roots = [action.withdrawroot for action in actions if action.event_name == WITHDRAWAL_COMPLETED_EVENT["name"]] + ac_map = dict() + with self.db_service.get_service_session() as session: + query = session.query(AfEigenLayerRecords).filter( + # func.encode(AfEigenLayerRecords.withdrawroot, "hex").in_(roots) + (AfEigenLayerRecords.withdrawroot).in_(roots) + ) + result = query.all() + for rr in result: + ac_map[rr.withdrawroot] = rr + for action in actions: + if action.event_name == WITHDRAWAL_COMPLETED_EVENT["name"]: + st = ac_map[action.withdrawroot] + action.shares = st.shares + action.strategy = bytes_to_hex_str(st.strategy) if st.strategy else None + action.token = bytes_to_hex_str(st.token) if st.token else None + action.staker = bytes_to_hex_str(st.staker) if st.staker else None + action.withdrawer = bytes_to_hex_str(st.withdrawer) if st.withdrawer else None + + def calculate_batch_result(self, eg_actions: List[EigenLayerActionD]) -> Any: + def nested_dict(): + return defaultdict(eigen_layer_address_current_factory) + + res_d = defaultdict(nested_dict) + for action in eg_actions: + staker = action.staker + strategy = action.strategy + token = action.token + topic0 = action.topic0 + if topic0 == self.eigen_layer_conf["DEPOSIT"]["topic"]: + res_d[staker][strategy].address = staker + res_d[staker][strategy].token = token + res_d[staker][strategy].strategy = strategy + res_d[staker][strategy].deposit_amount += action.shares + elif ( + topic0 == self.eigen_layer_conf["START_WITHDRAW"]["topic"] + or topic0 == self.eigen_layer_conf["START_WITHDRAW_2"]["topic"] + ): + res_d[staker][strategy].address = staker + res_d[staker][strategy].token = token + res_d[staker][strategy].strategy = strategy + res_d[staker][strategy].start_withdraw_amount += action.shares + elif topic0 == self.eigen_layer_conf["FINISH_WITHDRAW"]["topic"]: + res_d[staker][strategy].address = staker + res_d[staker][strategy].token = token + res_d[staker][strategy].strategy = strategy + res_d[staker][strategy].finish_withdraw_amount += action.shares + else: + raise FastShutdownError(f"eigen_layer_job Unexpected topic {topic0}") + return res_d + + +def create_nested_dict(data_list: List[EigenLayerAddressCurrentD]) -> Dict[str, Dict[str, EigenLayerAddressCurrentD]]: + result = {} + for item in data_list: + if item.address and item.strategy: + if item.address not in result: + result[item.address] = {} + result[item.address][item.strategy] = item + return result diff --git a/indexer/modules/custom/eigen_layer/models/__init__.py b/indexer/modules/custom/eigen_layer/models/__init__.py new file mode 100644 index 000000000..57f2c4c99 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/models/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:09 +# @Author will +# @File __init__.py.py +# @Brief diff --git a/indexer/modules/custom/eigen_layer/models/af_eigen_layer_address_current.py b/indexer/modules/custom/eigen_layer/models/af_eigen_layer_address_current.py new file mode 100644 index 000000000..5b7a23909 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/models/af_eigen_layer_address_current.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:13 +# @Author will +# @File af_eigen_layer_address_current.py +# @Brief +from sqlalchemy import Column, PrimaryKeyConstraint, func, text +from sqlalchemy.dialects.postgresql import BOOLEAN, BYTEA, NUMERIC, TIMESTAMP + +from common.models import HemeraModel, general_converter + + +class AfEigenLayerAddressCurrent(HemeraModel): + __tablename__ = "af_eigen_layer_address_current" + address = Column(BYTEA, primary_key=True) + + strategy = Column(BYTEA, primary_key=True) + token = Column(BYTEA) + + deposit_amount = Column(NUMERIC(100)) + start_withdraw_amount = Column(NUMERIC(100)) + finish_withdraw_amount = Column(NUMERIC(100)) + + d_s = Column(NUMERIC(100)) + d_f = Column(NUMERIC(100)) + s_f = Column(NUMERIC(100)) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now()) + reorg = Column(BOOLEAN, server_default=text("false")) + + __table_args__ = (PrimaryKeyConstraint("address", "strategy"),) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "EigenLayerAddressCurrentD", + "conflict_do_update": True, + "update_strategy": None, + "converter": general_converter, + } + ] diff --git a/indexer/modules/custom/eigen_layer/models/af_eigen_layer_records.py b/indexer/modules/custom/eigen_layer/models/af_eigen_layer_records.py new file mode 100644 index 000000000..089ff5ab0 --- /dev/null +++ b/indexer/modules/custom/eigen_layer/models/af_eigen_layer_records.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 18:29 +# @Author will +# @File af_eigen_layer_records.py +# @Brief +from sqlalchemy import Column, Numeric, PrimaryKeyConstraint, func, text +from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, BYTEA, INTEGER, NUMERIC, TIMESTAMP, VARCHAR + +from common.models import HemeraModel, general_converter + + +class AfEigenLayerRecords(HemeraModel): + __tablename__ = "af_eigen_layer_records" + transaction_hash = Column(BYTEA, primary_key=True) + log_index = Column(INTEGER, primary_key=True) + internal_idx = Column(INTEGER, primary_key=True) + block_number = Column(BIGINT) + block_timestamp = Column(BIGINT) + method = Column(VARCHAR) + event_name = Column(VARCHAR) + topic0 = Column(BYTEA) + from_address = Column(BYTEA) + to_address = Column(BYTEA) + + token = Column(BYTEA) + amount = Column(NUMERIC(100)) + balance = Column(NUMERIC(100)) + staker = Column(BYTEA) + operator = Column(BYTEA) + withdrawer = Column(BYTEA) + shares = Column(Numeric(100)) + withdrawroot = Column(BYTEA) + strategy = Column(BYTEA) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now()) + reorg = Column(BOOLEAN, server_default=text("false")) + + __table_args__ = ( + PrimaryKeyConstraint( + "transaction_hash", + "log_index", + "internal_idx", + ), + ) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "EigenLayerActionD", + "conflict_do_update": True, + "update_strategy": None, + "converter": general_converter, + } + ] From c0ff9cada71f103346d6cd9ae0e55df39d36e464 Mon Sep 17 00:00:00 2001 From: li xiang Date: Tue, 8 Oct 2024 13:16:47 +0800 Subject: [PATCH 02/12] feat(token_url): format token uri with quote_plus in pg (#177) --- common/models/erc1155_token_id_details.py | 4 +++- common/models/erc721_token_id_details.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/common/models/erc1155_token_id_details.py b/common/models/erc1155_token_id_details.py index 1a1fbcc3d..59c13fb02 100644 --- a/common/models/erc1155_token_id_details.py +++ b/common/models/erc1155_token_id_details.py @@ -1,9 +1,11 @@ from datetime import datetime +from typing import Type from sqlalchemy import Column, Index, PrimaryKeyConstraint, desc, func, text from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, BYTEA, JSONB, NUMERIC, TIMESTAMP, VARCHAR from common.models import HemeraModel, general_converter +from common.models.erc721_token_id_details import token_uri_format_converter class ERC1155TokenIdDetails(HemeraModel): @@ -31,7 +33,7 @@ def model_domain_mapping(): "domain": "ERC1155TokenIdDetail", "conflict_do_update": False, "update_strategy": None, - "converter": general_converter, + "converter": token_uri_format_converter, }, { "domain": "UpdateERC1155TokenIdDetail", diff --git a/common/models/erc721_token_id_details.py b/common/models/erc721_token_id_details.py index 94433d908..7beaec6c2 100644 --- a/common/models/erc721_token_id_details.py +++ b/common/models/erc721_token_id_details.py @@ -1,4 +1,6 @@ from datetime import datetime +from typing import Type +from urllib import parse from sqlalchemy import Column, Index, PrimaryKeyConstraint, desc, func, text from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, BYTEA, JSONB, NUMERIC, TIMESTAMP, VARCHAR @@ -6,6 +8,14 @@ from common.models import HemeraModel, general_converter +def token_uri_format_converter(table: Type[HemeraModel], data, is_update=False): + + if data.token_uri is not None: + data.token_uri = parse.quote_plus(data.token_uri) + + return general_converter(table, data, is_update) + + class ERC721TokenIdDetails(HemeraModel): __tablename__ = "erc721_token_id_details" @@ -31,7 +41,7 @@ def model_domain_mapping(): "domain": "ERC721TokenIdDetail", "conflict_do_update": False, "update_strategy": None, - "converter": general_converter, + "converter": token_uri_format_converter, }, { "domain": "UpdateERC721TokenIdDetail", From 07e4f82ad2b6ddadaa88bdb6254b573ac9692bfe Mon Sep 17 00:00:00 2001 From: xuzh2024 <167734725+xuzh2024@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:17:52 +0800 Subject: [PATCH 03/12] complete deposit support bridge info (#178) * complete deposit support bridge info --- api/app/utils/parse_utils.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/api/app/utils/parse_utils.py b/api/app/utils/parse_utils.py index c68202c28..a3ebad74c 100644 --- a/api/app/utils/parse_utils.py +++ b/api/app/utils/parse_utils.py @@ -6,7 +6,31 @@ "0x99c9fc46f92e8a1c0dec1b1747d010903e884be1": { "bridge_name": "Optimism Bridge", "bridge_logo": "https://storage.googleapis.com/socialscan-public-asset/bridge/optimism.png", - } + }, + "0x3154cf16ccdb4c6d922629664174b904d80f2c35": { + "bridge_name": "Base Bridge", + "bridge_logo": "https://www.base.org/_next/static/media/logoBlack.4dc25558.svg", + }, + "0x72ce9c846789fdb6fc1f34ac4ad25dd9ef7031ef": { + "bridge_name": "Arbitrum One: L1 Gateway Router", + "bridge_logo": "https://cryptologos.cc/logos/arbitrum-arb-logo.svg?v=035", + }, + "0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f": { + "bridge_name": "Arbitrum: Delayed Inbox", + "bridge_logo": "https://cryptologos.cc/logos/arbitrum-arb-logo.svg?v=035", + }, + "0x051f1d88f0af5763fb888ec4378b4d8b29ea3319": { + "bridge_name": "Linea: ERC20 Bridge", + "bridge_logo": "https://images.seeklogo.com/logo-png/52/1/linea-logo-png_seeklogo-527155.png", + }, + "0x504a330327a089d8364c4ab3811ee26976d388ce": { + "bridge_name": "Linea: USDC Bridge", + "bridge_logo": "https://images.seeklogo.com/logo-png/52/1/linea-logo-png_seeklogo-527155.png", + }, + "0xd19d4b5d358258f05d7b411e21a1460d11b0876f": { + "bridge_name": "Linea: L1 Message Service", + "bridge_logo": "https://images.seeklogo.com/logo-png/52/1/linea-logo-png_seeklogo-527155.png", + }, } From ea34aeba36788519f05458e41cc7b5daab57639f Mon Sep 17 00:00:00 2001 From: zhufengthehemera Date: Wed, 9 Oct 2024 11:56:41 +0800 Subject: [PATCH 04/12] Index cyber reverse name (#173) --- .gitignore | 1 + indexer/modules/custom/cyber_id/__init__.py | 0 .../cyber_id/abi/CyberIdReverseRegistrar.json | 4 + indexer/modules/custom/cyber_id/cyber_abi.py | 19 +++ .../modules/custom/cyber_id/cyber_domain.py | 32 ++++ .../custom/cyber_id/export_cyber_id_job.py | 138 ++++++++++++++++++ .../custom/cyber_id/models/__init__.py | 0 .../custom/cyber_id/models/cyber_models.py | 59 ++++++++ indexer/modules/custom/cyber_id/utils.py | 27 ++++ 9 files changed, 280 insertions(+) create mode 100644 indexer/modules/custom/cyber_id/__init__.py create mode 100644 indexer/modules/custom/cyber_id/abi/CyberIdReverseRegistrar.json create mode 100644 indexer/modules/custom/cyber_id/cyber_abi.py create mode 100644 indexer/modules/custom/cyber_id/cyber_domain.py create mode 100644 indexer/modules/custom/cyber_id/export_cyber_id_job.py create mode 100644 indexer/modules/custom/cyber_id/models/__init__.py create mode 100644 indexer/modules/custom/cyber_id/models/cyber_models.py create mode 100644 indexer/modules/custom/cyber_id/utils.py diff --git a/.gitignore b/.gitignore index 8ecfa07cb..8d5178316 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ resource/hemera.ini sync_record alembic.ini !indexer/modules/custom/hemera_ens/abi/*.json +!indexer/modules/custom/cyber_id/abi/*.json diff --git a/indexer/modules/custom/cyber_id/__init__.py b/indexer/modules/custom/cyber_id/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/indexer/modules/custom/cyber_id/abi/CyberIdReverseRegistrar.json b/indexer/modules/custom/cyber_id/abi/CyberIdReverseRegistrar.json new file mode 100644 index 000000000..b79622c40 --- /dev/null +++ b/indexer/modules/custom/cyber_id/abi/CyberIdReverseRegistrar.json @@ -0,0 +1,4 @@ +{ + "address": "0x79502da131357333d61c39b7411d01df54591961", + "abi":[{"inputs":[{"internalType":"contract ENS","name":"_ens","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"bool","name":"enabled","type":"bool"}],"name":"ControllerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract NameResolver","name":"resolver","type":"address"}],"name":"DefaultResolverChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ReverseClaimed","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"claim","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"}],"name":"claimForAddr","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"}],"name":"claimWithResolver","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"controllers","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"defaultResolver","outputs":[{"internalType":"contract NameResolver","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ens","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"node","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"controller","type":"address"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setController","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"resolver","type":"address"}],"name":"setDefaultResolver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"setName","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"string","name":"name","type":"string"}],"name":"setNameForAddr","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}] +} \ No newline at end of file diff --git a/indexer/modules/custom/cyber_id/cyber_abi.py b/indexer/modules/custom/cyber_id/cyber_abi.py new file mode 100644 index 000000000..fb514e61e --- /dev/null +++ b/indexer/modules/custom/cyber_id/cyber_abi.py @@ -0,0 +1,19 @@ +import json +import os + + +def get_absolute_path(relative_path): + current_dir = os.path.dirname(os.path.abspath(__file__)) + absolute_path = os.path.join(current_dir, relative_path) + return absolute_path + + +abi_map = {} + +relative_path = "abi" +absolute_path = get_absolute_path(relative_path) +fs = os.listdir(absolute_path) +for a_f in fs: + with open(os.path.join(absolute_path, a_f), "r") as data_file: + dic = json.load(data_file) + abi_map[dic["address"].lower()] = json.dumps(dic["abi"]) diff --git a/indexer/modules/custom/cyber_id/cyber_domain.py b/indexer/modules/custom/cyber_id/cyber_domain.py new file mode 100644 index 000000000..b9d37c939 --- /dev/null +++ b/indexer/modules/custom/cyber_id/cyber_domain.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +from numpy.distutils.fcompiler import none + +from indexer.domain import FilterData + + +@dataclass +class CyberAddressD(FilterData): + address: str + reverse_node: str + name: str + block_number: int + + +@dataclass +class CyberIDRegisterD(FilterData): + label: str + token_id: int + node: str + cost: int + block_number: int + registration: datetime + + +@dataclass +class CyberAddressChangedD(FilterData): + node: str + address: str + block_number: int diff --git a/indexer/modules/custom/cyber_id/export_cyber_id_job.py b/indexer/modules/custom/cyber_id/export_cyber_id_job.py new file mode 100644 index 000000000..d7b032af6 --- /dev/null +++ b/indexer/modules/custom/cyber_id/export_cyber_id_job.py @@ -0,0 +1,138 @@ +import logging +from itertools import groupby +from typing import List + +from web3 import Web3 + +from indexer.domain.log import Log +from indexer.domain.transaction import Transaction +from indexer.executors.batch_work_executor import BatchWorkExecutor +from indexer.jobs import FilterTransactionDataJob +from indexer.modules.custom.cyber_id.cyber_abi import abi_map +from indexer.modules.custom.cyber_id.cyber_domain import CyberAddressChangedD, CyberAddressD, CyberIDRegisterD +from indexer.modules.custom.cyber_id.utils import get_node, get_reverse_node +from indexer.specification.specification import TopicSpecification, TransactionFilterByLogs + +logger = logging.getLogger(__name__) + +CyberIdReverseRegistrarContractAddress = "0x79502da131357333d61c39b7411d01df54591961" +CyberIdPublicResolverContractAddress = "0xfb2f304c1fcd6b053ee033c03293616d5121944b" +CyberIdTokenContractAddress = "0xc137be6b59e824672aada673e55cf4d150669af8" +NameChangedTopic = "0xb7d29e911041e8d9b843369e890bcb72c9388692ba48b65ac54e7214c4c348f7" +RegisterTopic = "0xa50d98082663c2b716ab4f8b6b2a51fcaed7eae222cd3d74b19de4691ede728a" +AddressChangedTopic = "0x65412581168e88a1e60c6459d7f44ae83ad0832e670826c05a4e2476b57af752" + + +class ExportCyberIDJob(FilterTransactionDataJob): + dependency_types = [Transaction] + output_types = [CyberAddressD, CyberIDRegisterD, CyberAddressChangedD] + able_to_reorg = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self._batch_work_executor = BatchWorkExecutor( + kwargs["batch_size"], + kwargs["max_workers"], + job_name=self.__class__.__name__, + ) + self._is_batch = kwargs["batch_size"] > 1 + self._filters = kwargs.get("filters", []) + self.contract_object_map = {} + self.func_name_map = {} + self.w3 = Web3(Web3.HTTPProvider(self._web3.provider.endpoint_uri)) + for ad_lower in abi_map: + abi = abi_map[ad_lower] + contract = self.w3.eth.contract(address=Web3.to_checksum_address(ad_lower), abi=abi) + self.contract_object_map[ad_lower] = contract + for contract_address, contract in self.contract_object_map.items(): + if not contract: + continue + functions = [abi for abi in contract.abi if abi["type"] == "function"] + for function in functions: + sig = self.get_function_signature(function) + self.func_name_map[sig[0:10]] = function + + def get_filter(self): + + return [ + TransactionFilterByLogs( + [ + TopicSpecification( + addresses=[CyberIdPublicResolverContractAddress, CyberIdTokenContractAddress], + topics=[NameChangedTopic, RegisterTopic], + ) + ] + ), + ] + + def _collect(self, **kwargs): + transactions: List[Transaction] = self._data_buff.get(Transaction.type(), []) + for transaction in transactions: + if transaction.to_address.lower() == CyberIdReverseRegistrarContractAddress: + func_name = self.func_name_map.get(transaction.input[0:10], {}).get("name") + if func_name == "setNameForAddr": + decoded_input = self.decode_transaction(transaction) + cyber_address = CyberAddressD( + address=decoded_input[1].get("addr").lower(), + name=decoded_input[1].get("name"), + block_number=transaction.block_number, + reverse_node=get_reverse_node(decoded_input[1].get("addr")), + ) + self._collect_item(cyber_address.type(), cyber_address) + if func_name == "setName": + decoded_input = self.decode_transaction(transaction) + cyber_address = CyberAddressD( + address=transaction.from_address.lower(), + name=decoded_input[1].get("name"), + block_number=transaction.block_number, + reverse_node=get_reverse_node(transaction.from_address), + ) + self._collect_item(cyber_address.type(), cyber_address) + logs: List[Log] = self._data_buff.get(Log.type(), []) + for log in logs: + if log.address.lower() == CyberIdTokenContractAddress and log.topic0 == RegisterTopic: + decoded_data = self.w3.codec.decode(["string", "uint256"], bytes.fromhex(log.data[2:])) + cid = decoded_data[0] + cyber_address = CyberIDRegisterD( + label=cid, + token_id=log.topic3, + cost=int(decoded_data[1]), + block_number=log.block_number, + node=get_node(cid + ".cyber"), + registration=log.block_timestamp, + ) + self._collect_item(cyber_address.type(), cyber_address) + if log.address.lower() == CyberIdPublicResolverContractAddress and log.topic0 == AddressChangedTopic: + decoded_data = self.w3.codec.decode(["uint256", "bytes"], bytes.fromhex(log.data[2:])) + address_change_d = CyberAddressChangedD( + node=log.topic1, address="0x" + decoded_data[1].hex(), block_number=log.block_number + ) + self._collect_item(address_change_d.type(), address_change_d) + + def _process(self, **kwargs): + cyber_addresses = self._data_buff.get(CyberAddressD.type(), []) + cyber_addresses.sort(key=lambda x: (x.address, x.block_number)) + self._data_buff[CyberAddressD.type()] = [ + list(group)[-1] for key, group in groupby(cyber_addresses, key=lambda x: x.address) + ] + + address_changes = self._data_buff.get(CyberAddressChangedD.type(), []) + address_changes.sort(key=lambda x: (x.node, x.block_number)) + self._data_buff[CyberAddressChangedD.type()] = [ + list(group)[-1] for key, group in groupby(address_changes, key=lambda x: x.node) + ] + + def decode_transaction(self, transaction): + if not transaction.to_address: + return None + con = self.contract_object_map[transaction.to_address] + decoded_input = con.decode_function_input(transaction.input) + return decoded_input + + def get_function_signature(self, function_abi): + name = function_abi["name"] + inputs = [input["type"] for input in function_abi["inputs"]] + signature = f"{name}({','.join(inputs)})" + sig = self.w3.to_hex(Web3.keccak(text=signature)) + return sig diff --git a/indexer/modules/custom/cyber_id/models/__init__.py b/indexer/modules/custom/cyber_id/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/indexer/modules/custom/cyber_id/models/cyber_models.py b/indexer/modules/custom/cyber_id/models/cyber_models.py new file mode 100644 index 000000000..afd75e560 --- /dev/null +++ b/indexer/modules/custom/cyber_id/models/cyber_models.py @@ -0,0 +1,59 @@ +from sqlalchemy import Column, func +from sqlalchemy.dialects.postgresql import BIGINT, BYTEA, NUMERIC, TIMESTAMP, VARCHAR + +from common.models import HemeraModel +from indexer.modules.custom.hemera_ens.models.af_ens_node_current import ens_general_converter + + +class CyberAddress(HemeraModel): + __tablename__ = "cyber_address" + + address = Column(BYTEA, primary_key=True) + name = Column(VARCHAR) + reverse_node = Column(BYTEA) + block_number = Column(BIGINT) + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now()) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "CyberAddressD", + "conflict_do_update": True, + "update_strategy": "EXCLUDED.block_number > cyber_address.block_number", + "converter": ens_general_converter, + } + ] + + +class CyberIDRecord(HemeraModel): + __tablename__ = "cyber_id_record" + + node = Column(BYTEA, primary_key=True) + token_id = Column(NUMERIC(100)) + label = Column(VARCHAR) + registration = Column(TIMESTAMP) + address = Column(BYTEA) + block_number = Column(BIGINT) + cost = Column(NUMERIC(100)) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now()) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "CyberIDRegisterD", + "conflict_do_update": None, + "update_strategy": None, + "converter": ens_general_converter, + }, + { + "domain": "CyberAddressChangedD", + "conflict_do_update": True, + "update_strategy": "EXCLUDED.block_number >= cyber_id_record.block_number", + "converter": ens_general_converter, + }, + ] diff --git a/indexer/modules/custom/cyber_id/utils.py b/indexer/modules/custom/cyber_id/utils.py new file mode 100644 index 000000000..8ec969257 --- /dev/null +++ b/indexer/modules/custom/cyber_id/utils.py @@ -0,0 +1,27 @@ +from ens.auto import ns +from ens.constants import EMPTY_SHA3_BYTES +from ens.utils import Web3, address_to_reverse_domain, is_empty_name, normal_name_to_hash, normalize_name +from hexbytes import HexBytes + + +def get_reverse_node(address): + address = address_to_reverse_domain(address) + return ns.namehash(address) + + +def label_to_hash(label: str) -> HexBytes: + if "." in label: + raise ValueError(f"Cannot generate hash for label {label!r} with a '.'") + return Web3().keccak(text=label) + + +def get_node(name): + node = EMPTY_SHA3_BYTES + if not is_empty_name(name): + labels = name.split(".") + for label in reversed(labels): + label_hash = label_to_hash(label) + assert isinstance(label_hash, bytes) + assert isinstance(node, bytes) + node = Web3().keccak(node + label_hash) + return node.hex() From b5b95891f179ac13313ec7931dda8480b20d29bf Mon Sep 17 00:00:00 2001 From: li xiang Date: Thu, 10 Oct 2024 13:11:34 +0800 Subject: [PATCH 05/12] Optimize version handling and add AWS image push action (#180) * Optimize version handling and add AWS image push action - Improve version-related code - Add new action for automatic image push to AWS - Add command make development --- .github/workflows/push-image.yaml | 85 ++ Makefile | 70 +- README.md | 50 +- __init__.py | 5 - docs/README.md | 85 +- poetry.lock | 1197 ++++++++++++++--------------- pyproject.toml | 10 +- 7 files changed, 816 insertions(+), 686 deletions(-) create mode 100644 .github/workflows/push-image.yaml delete mode 100644 __init__.py diff --git a/.github/workflows/push-image.yaml b/.github/workflows/push-image.yaml new file mode 100644 index 000000000..aeb9d1c89 --- /dev/null +++ b/.github/workflows/push-image.yaml @@ -0,0 +1,85 @@ +name: Push image to AWS ECR + +on: + push: + branches: + - master + tags: + - 'v*' + workflow_dispatch: + inputs: + runPushAWS: + description: 'Run push image to aws (yes/no)' + required: true + default: 'false' + arch: + required: false + default: 'amd64' + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set arch variable + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "ARCH=${{ github.event.inputs.arch }}" >> $GITHUB_ENV + else + echo "ARCH=amd64" >> $GITHUB_ENV + fi + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Configure AWS CLI profile + run: | + aws configure set aws_access_key_id ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} --profile prod + aws configure set aws_secret_access_key ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }} --profile prod + + - name: Build and Push to AWS ECR + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') || github.event.inputs.runPushAWS == 'yes' + env: + ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + ECR_REPO: hemera-protocol + run: | + echo "Architecture: ${{ env.ARCH }}" + echo "Building and pushing to AWS ECR" + + if [[ $GITHUB_REF == refs/tags/* ]]; then + # It's a tag push, use the tag as is + TAG=${GITHUB_REF#refs/tags/} + # Remove 'v' prefix if present + TAG=${TAG#v} + else + # Use the original naming convention + VERSION=$(grep '^version = ' pyproject.toml | sed 's/^version = //;s/"//g') + if [[ $GITHUB_EVENT_NAME == "pull_request" ]]; then + # It's a pull request + BUILD=$(echo ${{ github.event.pull_request.head.sha }} | cut -c 1-7) + else + # It's a push to a branch (e.g., master) + BUILD=$(git rev-parse --short=7 HEAD) + fi + TAG=$VERSION-$BUILD-${{ env.ARCH }} + fi + + echo "Tag: $TAG" + + # Build the Docker image using make + make image TAG=$TAG ARCH=${{ env.ARCH }} + + # Login to ECR + aws ecr get-login-password --region ${{ secrets.AWS_REGION }} --profile prod | docker login --username AWS --password-stdin $ECR_REGISTRY + + # Tag the image for ECR + docker tag $ECR_REPO:$TAG $ECR_REGISTRY/$ECR_REPO:$TAG + + # Push the image to ECR + docker push $ECR_REGISTRY/$ECR_REPO:$TAG \ No newline at end of file diff --git a/Makefile b/Makefile index 1acb04f4b..964c58414 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,29 @@ -VERSION := $(shell poetry version -s) +ARCH=amd64 +VERSION := $(shell grep '^version = ' pyproject.toml | sed 's/^version = //;s/"//g') BUILD := `git rev-parse --short=7 HEAD` -SERVICES = -.PHONY: all build image test + +TAG := $(VERSION)-$(BUILD)-$(ARCH) + + +PRE_COMMIT_INSTALLED := $(shell command -v pre-commit > /dev/null 2>&1 && echo yes || echo no) +VENV_DIR := .venv +POETRY_INSTALLED := $(shell command -v poetry > /dev/null 2>&1 && echo yes || echo no) + +IMAGE_FLAGS := $(IMAGE_FLAGS) --platform linux/$(ARCH) RED=\033[31m GREEN=\033[32m YELLOW=\033[33m RESET=\033[0m -image: - docker build $(IMAGE_FLAGS) --network host -t hemera-protocol:$(VERSION)-$(BUILD) . --no-cache - echo "Built image hemera-protocol:$(VERSION)-$(BUILD)" +.PHONY: format init_db development image test + +image: + @echo "Build tag: $(TAG)" + @echo "Build flags: $(IMAGE_FLAGS)" + docker buildx build $(IMAGE_FLAGS) --network host -t hemera-protocol:$(TAG) . --no-cache + @echo "Built image hemera-protocol:$(TAG)" test: @if [ "$(filter-out $@,$(MAKECMDGOALS))" = "" ]; then \ @@ -21,8 +33,6 @@ test: fi -PRE_COMMIT_INSTALLED := $(shell command -v pre-commit > /dev/null 2>&1 && echo yes || echo no) - format: ifeq ($(PRE_COMMIT_INSTALLED),yes) @echo "$(YELLOW)Formatting code...$(RESET)" @@ -33,4 +43,46 @@ endif init_db: @echo "Initializing database..." - poetry run python -m hemera.py init_db \ No newline at end of file + poetry run python -m hemera.py init_db + +development: + @echo "Setting up development environment..." + @bash -c 'set -euo pipefail; \ + PYTHON_CMD=$$(command -v python3 || command -v python); \ + if [ -z "$$PYTHON_CMD" ] || ! "$$PYTHON_CMD" --version 2>&1 | grep -q "Python 3"; then \ + echo "Python 3 is not found. Please install Python 3 and try again."; \ + exit 1; \ + fi; \ + python_version=$$($$PYTHON_CMD -c "import sys; print(\"{}.{}\".format(sys.version_info.major, sys.version_info.minor))"); \ + if ! echo "$$python_version" | grep -qE "^3\.(8|9|10|11)"; then \ + echo "Python version $$python_version is not supported. Please use Python 3.8, 3.9, 3.10, or 3.11."; \ + exit 1; \ + fi; \ + echo "Using Python: $$($$PYTHON_CMD --version)"; \ + if [ ! -d ".venv" ]; then \ + echo "Creating virtual environment..."; \ + $$PYTHON_CMD -m venv .venv || { \ + echo "Failed to create virtual environment. Installing venv..."; \ + sudo apt-get update && sudo apt-get install -y python3-venv && $$PYTHON_CMD -m venv .venv; \ + }; \ + fi; \ + echo "Activating virtual environment..."; \ + . .venv/bin/activate; \ + if ! pip --version &> /dev/null; then \ + echo "Installing pip..."; \ + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py; \ + $$PYTHON_CMD get-pip.py; \ + rm get-pip.py; \ + fi; \ + if ! poetry --version &> /dev/null; then \ + echo "Installing Poetry..."; \ + pip install poetry; \ + else \ + echo "Poetry is already installed."; \ + fi; \ + echo "Installing project dependencies..."; \ + poetry install -v; \ + echo "Development environment setup complete."; \ + echo ""; \ + echo "To activate the virtual environment, run:"; \ + echo "source .venv/bin/activate"' \ No newline at end of file diff --git a/README.md b/README.md index 0c3ecdace..78f860726 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ As of July 5, 2024, the initial open-source version of the Hemera Indexer offers - Logs - ERC20 / ERC721 / ERC1155 tokens - ERC20 / ERC721 / ERC1155 Token transfers -- ERC20 / ERC721 / ERC1155 Token balance & holders +- ERC20 / ERC721 / ERC1155 Token balance - Contracts - Traces / Internal transactions - L1 -> L2 Transactions @@ -114,6 +114,7 @@ If you have trouble running the following commands, consider referring to the [official docker installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) for the latest instructions. +##### Ubuntu and Debian ```bash # Add Docker's official GPG key: sudo apt-get update @@ -128,11 +129,29 @@ echo \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -``` -```bash # Install docker and docker compose sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +docker compose version +``` + +##### RPM-based distros +```bash +sudo yum update -y +sudo yum install docker -y +sudo service docker start +sudo systemctl enable docker +sudo usermod -a -G docker ec2-user + +newgrp docker +docker --version +docker run hello-world + +DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} +mkdir -p $DOCKER_CONFIG/cli-plugins +curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose +chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose +docker compose version ``` #### Run the Docker Compose @@ -169,33 +188,38 @@ Attaching to hemera-api, indexer, indexer-trace, postgresql, redis ### Run From Source Code -#### Install Python3 and Pip +#### Install developer tools Skip this step if you already have both installed. ```bash sudo apt update -sudo apt install python3 -sudo apt install python3-pip +sudo apt install make ``` -#### Initiate Python VENV +#### Run development -Skip this step if you don't want to have a dedicated python venv for Hemera Indexer. +To deploy your project, simply run: ```bash -sudo apt install python3-venv -python3 -m venv ./venv +make development ``` -#### Install Pip Dependencies +This command will: +1. Create a Python virtual environment +2. Activate the virtual environment +3. Install necessary system packages +4. Install Python dependencies + +After running this command, your environment will be set up and ready to use. + +Remember to activate the virtual environment (`source ./venv/bin/activate`) when you want to work on your project in the future. ```bash source ./venv/bin/activate -sudo apt install libpq-dev -pip install -e . ``` + #### Prepare Your PostgreSQL Instance Hemera Indexer requires a PostgreSQL database to store all indexed data. You may skip this step if you already have a PostgreSQL set up. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 41ad41f70..000000000 --- a/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from pathlib import Path - -import tomli - -__version__ = "0.3.0" diff --git a/docs/README.md b/docs/README.md index 2c3d418f4..9ff99e6ca 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,11 +101,6 @@ or git clone git@github.com:HemeraProtocol/hemera-indexer.git ``` -### Run Hemera Indexer - -We recommend running from docker containers using the provided `docker-compose.yaml` . -If you prefer running from source code, please check out [Run From Source Code](#run-from-source-code). - ### Run In Docker #### Install Docker & Docker Compose @@ -114,6 +109,7 @@ If you have trouble running the following commands, consider referring to the [official docker installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) for the latest instructions. +##### Ubuntu and Debian ```bash # Add Docker's official GPG key: sudo apt-get update @@ -128,11 +124,29 @@ echo \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -``` -```bash # Install docker and docker compose sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +docker compose version +``` + +##### RPM-based distros +```bash +sudo yum update -y +sudo yum install docker -y +sudo service docker start +sudo systemctl enable docker +sudo usermod -a -G docker ec2-user + +newgrp docker +docker --version +docker run hello-world + +DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} +mkdir -p $DOCKER_CONFIG/cli-plugins +curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose +chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose +docker compose version ``` #### Run the Docker Compose @@ -158,62 +172,49 @@ sudo docker compose up You should be able to see similar logs from your console that indicate Hemera Indexer is running properly. ``` -[+] Running 2/0 - ✔ Container postgresql Created 0.0s - ✔ Container hemera Created 0.0s -Attaching to hemera, postgresql -postgresql | -postgresql | PostgreSQL Database directory appears to contain a database; Skipping initialization -postgresql | -postgresql | 2024-06-24 08:18:48.547 UTC [1] LOG: starting PostgreSQL 15.7 (Debian 15.7-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit -postgresql | 2024-06-24 08:18:48.548 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 -postgresql | 2024-06-24 08:18:48.549 UTC [1] LOG: listening on IPv6 address "::", port 5432 -postgresql | 2024-06-24 08:18:48.554 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" -postgresql | 2024-06-24 08:18:48.564 UTC [27] LOG: database system was shut down at 2024-06-24 06:44:23 UTC -postgresql | 2024-06-24 08:18:48.575 UTC [1] LOG: database system is ready to accept connections -hemera | 2024-06-24 08:18:54,953 - root [INFO] - Using provider https://eth.llamarpc.com -hemera | 2024-06-24 08:18:54,953 - root [INFO] - Using debug provider https://eth.llamarpc.com -hemera | 2024-06-24 08:18:55,229 - alembic.runtime.migration [INFO] - Context impl PostgresqlImpl. -hemera | 2024-06-24 08:18:55,230 - alembic.runtime.migration [INFO] - Will assume transactional DDL. -hemera | 2024-06-24 08:18:55,278 - alembic.runtime.migration [INFO] - Context impl PostgresqlImpl. -hemera | 2024-06-24 08:18:55,278 - alembic.runtime.migration [INFO] - Will assume transactional DDL. -hemera | 2024-06-24 08:18:56,169 - root [INFO] - Current block 20160303, target block 20137200, last synced block 20137199, blocks to sync 1 -hemera | 2024-06-24 08:18:56,170 - ProgressLogger [INFO] - Started work. Items to process: 1. -hemera | 2024-06-24 08:18:57,505 - ProgressLogger [INFO] - 1 items processed. Progress is 100%. -hemera | 2024-06-24 08:18:57,506 - ProgressLogger [INFO] - Finished work. Total items processed: 1. Took 0:00:01.336310. -hemera | 2024-06-24 08:18:57,529 - exporters.postgres_item_exporter [INFO] - Exporting items to table block_ts_mapper, blocks end, Item count: 2, Took 0:00:00.022562 -hemera | 2024-06-24 08:18:57,530 - ProgressLogger [INFO] - Started work. +[+] Running 5/0 + ✔ Container redis Created 0.0s + ✔ Container postgresql Created 0.0s + ✔ Container indexer Created 0.0s + ✔ Container indexer-trace Created 0.0s + ✔ Container hemera-api Created 0.0s +Attaching to hemera-api, indexer, indexer-trace, postgresql, redis ``` ### Run From Source Code -#### Install Python3 and Pip +#### Install developer tools Skip this step if you already have both installed. ```bash sudo apt update -sudo apt install python3 -sudo apt install python3-pip +sudo apt install make ``` -#### Initiate Python VENV +#### Run development -Skip this step if you don't want to have a dedicated python venv for Hemera Indexer. +To deploy your project, simply run: ```bash -sudo apt install python3-venv -python3 -m venv ./venv +make development ``` -#### Install Pip Dependencies +This command will: +1. Create a Python virtual environment +2. Activate the virtual environment +3. Install necessary system packages +4. Install Python dependencies + +After running this command, your environment will be set up and ready to use. + +Remember to activate the virtual environment (`source ./venv/bin/activate`) when you want to work on your project in the future. ```bash source ./venv/bin/activate -sudo apt install libpq-dev -pip install -e . ``` + #### Prepare Your PostgreSQL Instance Hemera Indexer requires a PostgreSQL database to store all indexed data. You may skip this step if you already have a PostgreSQL set up. diff --git a/poetry.lock b/poetry.lock index f10379cab..0a749460a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,113 +2,113 @@ [[package]] name = "aiohappyeyeballs" -version = "2.4.0" +version = "2.4.3" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, - {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, + {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, + {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, ] [[package]] name = "aiohttp" -version = "3.10.6" +version = "3.10.9" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd"}, - {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b"}, - {file = "aiohttp-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593"}, - {file = "aiohttp-3.10.6-cp310-cp310-win32.whl", hash = "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791"}, - {file = "aiohttp-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f"}, - {file = "aiohttp-3.10.6-cp311-cp311-win32.whl", hash = "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27"}, - {file = "aiohttp-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0"}, - {file = "aiohttp-3.10.6-cp312-cp312-win32.whl", hash = "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883"}, - {file = "aiohttp-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362"}, - {file = "aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5"}, - {file = "aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962"}, - {file = "aiohttp-3.10.6-cp38-cp38-win32.whl", hash = "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98"}, - {file = "aiohttp-3.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93"}, - {file = "aiohttp-3.10.6-cp39-cp39-win32.whl", hash = "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db"}, - {file = "aiohttp-3.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb"}, - {file = "aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336"}, + {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3fb28a9ac8f2558760d8e637dbf27aef1e8b7f1d221e8669a1074d1a266bb2"}, + {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91aa966858593f64c8a65cdefa3d6dc8fe3c2768b159da84c1ddbbb2c01ab4ef"}, + {file = "aiohttp-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63649309da83277f06a15bbdc2a54fbe75efb92caa2c25bb57ca37762789c746"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e7fabedb3fe06933f47f1538df7b3a8d78e13d7167195f51ca47ee12690373"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c070430fda1a550a1c3a4c2d7281d3b8cfc0c6715f616e40e3332201a253067"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:51d0a4901b27272ae54e42067bc4b9a90e619a690b4dc43ea5950eb3070afc32"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fec5fac7aea6c060f317f07494961236434928e6f4374e170ef50b3001e14581"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:172ad884bb61ad31ed7beed8be776eb17e7fb423f1c1be836d5cb357a096bf12"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d646fdd74c25bbdd4a055414f0fe32896c400f38ffbdfc78c68e62812a9e0257"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e86260b76786c28acf0b5fe31c8dca4c2add95098c709b11e8c35b424ebd4f5b"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d7cafc11d70fdd8801abfc2ff276744ae4cb39d8060b6b542c7e44e5f2cfc2"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc262c3df78c8ff6020c782d9ce02e4bcffe4900ad71c0ecdad59943cba54442"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:482c85cf3d429844396d939b22bc2a03849cb9ad33344689ad1c85697bcba33a"}, + {file = "aiohttp-3.10.9-cp310-cp310-win32.whl", hash = "sha256:aeebd3061f6f1747c011e1d0b0b5f04f9f54ad1a2ca183e687e7277bef2e0da2"}, + {file = "aiohttp-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:fa430b871220dc62572cef9c69b41e0d70fcb9d486a4a207a5de4c1f25d82593"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16e6a51d8bc96b77f04a6764b4ad03eeef43baa32014fce71e882bd71302c7e4"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8bd9125dd0cc8ebd84bff2be64b10fdba7dc6fd7be431b5eaf67723557de3a31"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dcf354661f54e6a49193d0b5653a1b011ba856e0b7a76bda2c33e4c6892f34ea"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42775de0ca04f90c10c5c46291535ec08e9bcc4756f1b48f02a0657febe89b10"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d1e4185c5d7187684d41ebb50c9aeaaaa06ca1875f4c57593071b0409d2444"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2695c61cf53a5d4345a43d689f37fc0f6d3a2dc520660aec27ec0f06288d1f9"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a3f063b41cc06e8d0b3fcbbfc9c05b7420f41287e0cd4f75ce0a1f3d80729e6"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d37f4718002863b82c6f391c8efd4d3a817da37030a29e2682a94d2716209de"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2746d8994ebca1bdc55a1e998feff4e94222da709623bb18f6e5cfec8ec01baf"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6f3c6648aa123bcd73d6f26607d59967b607b0da8ffcc27d418a4b59f4c98c7c"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:558b3d223fd631ad134d89adea876e7fdb4c93c849ef195049c063ada82b7d08"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4e6cb75f8ddd9c2132d00bc03c9716add57f4beff1263463724f6398b813e7eb"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:608cecd8d58d285bfd52dbca5b6251ca8d6ea567022c8a0eaae03c2589cd9af9"}, + {file = "aiohttp-3.10.9-cp311-cp311-win32.whl", hash = "sha256:36d4fba838be5f083f5490ddd281813b44d69685db910907636bc5dca6322316"}, + {file = "aiohttp-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:8be1a65487bdfc285bd5e9baf3208c2132ca92a9b4020e9f27df1b16fab998a9"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4fd16b30567c5b8e167923be6e027eeae0f20cf2b8a26b98a25115f28ad48ee0"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:40ff5b7660f903dc587ed36ef08a88d46840182d9d4b5694e7607877ced698a1"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4edc3fd701e2b9a0d605a7b23d3de4ad23137d23fc0dbab726aa71d92f11aaaf"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e525b69ee8a92c146ae5b4da9ecd15e518df4d40003b01b454ad694a27f498b5"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5002a02c17fcfd796d20bac719981d2fca9c006aac0797eb8f430a58e9d12431"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4ceeae2fb8cabdd1b71c82bfdd39662473d3433ec95b962200e9e752fb70d0"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e395c3d1f773cf0651cd3559e25182eb0c03a2777b53b4575d8adc1149c6e9"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbdb8def5268f3f9cd753a265756f49228a20ed14a480d151df727808b4531dd"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f82ace0ec57c94aaf5b0e118d4366cff5889097412c75aa14b4fd5fc0c44ee3e"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ebdc3b3714afe1b134b3bbeb5f745eed3ecbcff92ab25d80e4ef299e83a5465"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f9ca09414003c0e96a735daa1f071f7d7ed06962ef4fa29ceb6c80d06696d900"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1298b854fd31d0567cbb916091be9d3278168064fca88e70b8468875ef9ff7e7"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60ad5b8a7452c0f5645c73d4dad7490afd6119d453d302cd5b72b678a85d6044"}, + {file = "aiohttp-3.10.9-cp312-cp312-win32.whl", hash = "sha256:1a0ee6c0d590c917f1b9629371fce5f3d3f22c317aa96fbdcce3260754d7ea21"}, + {file = "aiohttp-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:c46131c6112b534b178d4e002abe450a0a29840b61413ac25243f1291613806a"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bd9f3eac515c16c4360a6a00c38119333901b8590fe93c3257a9b536026594d"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8cc0d13b4e3b1362d424ce3f4e8c79e1f7247a00d792823ffd640878abf28e56"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba1a599255ad6a41022e261e31bc2f6f9355a419575b391f9655c4d9e5df5ff5"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:776e9f3c9b377fcf097c4a04b241b15691e6662d850168642ff976780609303c"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8debb45545ad95b58cc16c3c1cc19ad82cffcb106db12b437885dbee265f0ab5"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2555e4949c8d8782f18ef20e9d39730d2656e218a6f1a21a4c4c0b56546a02e"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c54dc329cd44f7f7883a9f4baaefe686e8b9662e2c6c184ea15cceee587d8d69"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e709d6ac598c5416f879bb1bae3fd751366120ac3fa235a01de763537385d036"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:17c272cfe7b07a5bb0c6ad3f234e0c336fb53f3bf17840f66bd77b5815ab3d16"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c21c82df33b264216abffff9f8370f303dab65d8eee3767efbbd2734363f677"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9331dd34145ff105177855017920dde140b447049cd62bb589de320fd6ddd582"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ac3196952c673822ebed8871cf8802e17254fff2a2ed4835d9c045d9b88c5ec7"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2c33fa6e10bb7ed262e3ff03cc69d52869514f16558db0626a7c5c61dde3c29f"}, + {file = "aiohttp-3.10.9-cp313-cp313-win32.whl", hash = "sha256:a14e4b672c257a6b94fe934ee62666bacbc8e45b7876f9dd9502d0f0fe69db16"}, + {file = "aiohttp-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:a35ed3d03910785f7d9d6f5381f0c24002b2b888b298e6f941b2fc94c5055fcd"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f392ef50e22c31fa49b5a46af7f983fa3f118f3eccb8522063bee8bfa6755f8"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d1f5c9169e26db6a61276008582d945405b8316aae2bb198220466e68114a0f5"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8d9d10d10ec27c0d46ddaecc3c5598c4db9ce4e6398ca872cdde0525765caa2f"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d97273a52d7f89a75b11ec386f786d3da7723d7efae3034b4dda79f6f093edc1"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d271f770b52e32236d945911b2082f9318e90ff835d45224fa9e28374303f729"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7003f33f5f7da1eb02f0446b0f8d2ccf57d253ca6c2e7a5732d25889da82b517"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6e00c8a92e7663ed2be6fcc08a2997ff06ce73c8080cd0df10cc0321a3168d7"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a61df62966ce6507aafab24e124e0c3a1cfbe23c59732987fc0fd0d71daa0b88"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:60555211a006d26e1a389222e3fab8cd379f28e0fbf7472ee55b16c6c529e3a6"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d15a29424e96fad56dc2f3abed10a89c50c099f97d2416520c7a543e8fddf066"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a19caae0d670771ea7854ca30df76f676eb47e0fd9b2ee4392d44708f272122d"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:99f9678bf0e2b1b695e8028fedac24ab6770937932eda695815d5a6618c37e04"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2914caa46054f3b5ff910468d686742ff8cff54b8a67319d75f5d5945fd0a13d"}, + {file = "aiohttp-3.10.9-cp38-cp38-win32.whl", hash = "sha256:0bc059ecbce835630e635879f5f480a742e130d9821fbe3d2f76610a6698ee25"}, + {file = "aiohttp-3.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:e883b61b75ca6efc2541fcd52a5c8ccfe288b24d97e20ac08fdf343b8ac672ea"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fcd546782d03181b0b1d20b43d612429a90a68779659ba8045114b867971ab71"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:85711eec2d875cd88c7eb40e734c4ca6d9ae477d6f26bd2b5bb4f7f60e41b156"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02d1d6610588bcd743fae827bd6f2e47e0d09b346f230824b4c6fb85c6065f9c"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3668d0c2a4d23fb136a753eba42caa2c0abbd3d9c5c87ee150a716a16c6deec1"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7c071235a47d407b0e93aa6262b49422dbe48d7d8566e1158fecc91043dd948"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac74e794e3aee92ae8f571bfeaa103a141e409863a100ab63a253b1c53b707eb"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bbf94d4a0447705b7775417ca8bb8086cc5482023a6e17cdc8f96d0b1b5aba6"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb0b2d5d51f96b6cc19e6ab46a7b684be23240426ae951dcdac9639ab111b45e"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e83dfefb4f7d285c2d6a07a22268344a97d61579b3e0dce482a5be0251d672ab"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f0a44bb40b6aaa4fb9a5c1ee07880570ecda2065433a96ccff409c9c20c1624a"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c2b627d3c8982691b06d89d31093cee158c30629fdfebe705a91814d49b554f8"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:03690541e4cc866eef79626cfa1ef4dd729c5c1408600c8cb9e12e1137eed6ab"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad3675c126f2a95bde637d162f8231cff6bc0bc9fbe31bd78075f9ff7921e322"}, + {file = "aiohttp-3.10.9-cp39-cp39-win32.whl", hash = "sha256:1321658f12b6caffafdc35cfba6c882cb014af86bef4e78c125e7e794dfb927b"}, + {file = "aiohttp-3.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:9fdf5c839bf95fc67be5794c780419edb0dbef776edcfc6c2e5e2ffd5ee755fa"}, + {file = "aiohttp-3.10.9.tar.gz", hash = "sha256:143b0026a9dab07a05ad2dd9e46aa859bffdd6348ddc5967b42161168c24f857"}, ] [package.dependencies] @@ -397,101 +397,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -615,115 +630,97 @@ files = [ [[package]] name = "cytoolz" -version = "0.12.3" +version = "1.0.0" description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346"}, - {file = "cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5"}, - {file = "cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd"}, - {file = "cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2"}, - {file = "cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605"}, - {file = "cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887"}, - {file = "cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f"}, - {file = "cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6"}, - {file = "cytoolz-0.12.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9a38332cfad2a91e89405b7c18b3f00e2edc951c225accbc217597d3e4e9fde"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f501ae1353071fa5d6677437bbeb1aeb5622067dce0977cedc2c5ec5843b202"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56f899758146a52e2f8cfb3fb6f4ca19c1e5814178c3d584de35f9e4d7166d91"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800f0526adf9e53d3c6acda748f4def1f048adaa780752f154da5cf22aa488a2"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0976a3fcb81d065473173e9005848218ce03ddb2ec7d40dd6a8d2dba7f1c3ae"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c835eab01466cb67d0ce6290601ebef2d82d8d0d0a285ed0d6e46989e4a7a71a"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fba0616fcd487e34b8beec1ad9911d192c62e758baa12fcb44448b9b6feae22"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f6e8207d732651e0204779e1ba5a4925c93081834570411f959b80681f8d333"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8119bf5961091cfe644784d0bae214e273b3b3a479f93ee3baab97bbd995ccfe"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7ad1331cb68afeec58469c31d944a2100cee14eac221553f0d5218ace1a0b25d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:92c53d508fb8a4463acc85b322fa24734efdc66933a5c8661bdc862103a3373d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:2c6dd75dae3d84fa8988861ab8b1189d2488cb8a9b8653828f9cd6126b5e7abd"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:caf07a97b5220e6334dd32c8b6d8b2bd255ca694eca5dfe914bb5b880ee66cdb"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed0cfb9326747759e2ad81cb6e45f20086a273b67ac3a4c00b19efcbab007c60"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:96a5a0292575c3697121f97cc605baf2fd125120c7dcdf39edd1a135798482ca"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76f2f50a789c44d6fd7f773ec43d2a8686781cd52236da03f7f7d7998989bee"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905fdccacc64b4beba37f95cab9d792289c80f4d70830b70de2fc66c007ec01"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ebe23028eac51251f22ba01dba6587d30aa9c320372ca0c14eeab67118ec3f"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c715404a3825e37fe3966fe84c5f8a1f036e7640b2a02dbed96cac0c933451"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac0adffc1b6b6a4c5f1fd1dd2161afb720bcc771a91016dc6bdba59af0a5d3"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:37441bf4a2a4e2e0fe9c3b0ea5e72db352f5cca03903977ffc42f6f6c5467be9"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f04037302049cb30033f7fa4e1d0e44afe35ed6bfcf9b380fc11f2a27d3ed697"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f37b60e66378e7a116931d7220f5352186abfcc950d64856038aa2c01944929c"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ec9be3e4b6f86ea8b294d34c990c99d2ba6c526ef1e8f46f1d52c263d4f32cd7"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e9199c9e3fbf380a92b8042c677eb9e7ed4bccb126de5e9c0d26f5888d96788"}, - {file = "cytoolz-0.12.3-cp38-cp38-win32.whl", hash = "sha256:18cd61e078bd6bffe088e40f1ed02001387c29174750abce79499d26fa57f5eb"}, - {file = "cytoolz-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:765b8381d4003ceb1a07896a854eee2c31ebc950a4ae17d1e7a17c2a8feb2a68"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4a52dd2a36b0a91f7aa50ca6c8509057acc481a24255f6cb07b15d339a34e0f"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:581f1ce479769fe7eeb9ae6d87eadb230df8c7c5fff32138162cdd99d7fb8fc3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f505d4c6eb79585c8ad0b9dc140ef30a138c880e4e3b40230d642690e36366"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59276021619b432a5c21c01cda8320b9cc7dbc40351ffc478b440bfccd5bbdd3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e44f4c25e1e7cf6149b499c74945a14649c8866d36371a2c2d2164e4649e7755"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64f8e60c1dd69e4d5e615481f2d57937746f4a6be2d0f86e9e7e3b9e2243b5e"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c63186f3bf9d7ef1347bc0537bb9a0b4111a0d7d6e619623cabc18fef0dc3b"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fdddb9d988405f24035234f1e8d1653ab2e48cc2404226d21b49a129aefd1d25"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6986632d8a969ea1e720990c818dace1a24c11015fd7c59b9fea0b65ef71f726"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0ba1cbc4d9cd7571c917f88f4a069568e5121646eb5d82b2393b2cf84712cf2a"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7d267ffc9a36c0a9a58c7e0adc9fa82620f22e4a72533e15dd1361f57fc9accf"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95e878868a172a41fbf6c505a4b967309e6870e22adc7b1c3b19653d062711fa"}, - {file = "cytoolz-0.12.3-cp39-cp39-win32.whl", hash = "sha256:8e21932d6d260996f7109f2a40b2586070cb0a0cf1d65781e156326d5ebcc329"}, - {file = "cytoolz-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:0d8edfbc694af6c9bda4db56643fb8ed3d14e47bec358c2f1417de9a12d6d1fb"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cd88028bb897fba99ddd84f253ca6bef73ecb7bdf3f3cf25bc493f8f97d3c7c5"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b19223e7f7bd7a73ec3aa6fdfb73b579ff09c2bc0b7d26857eec2d01a58c76"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a79d72b08048a0980a59457c239555f111ac0c8bdc140c91a025f124104dbb4"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd70141b32b717696a72b8876e86bc9c6f8eff995c1808e299db3541213ff82"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a1445c91009eb775d479e88954c51d0b4cf9a1e8ce3c503c2672d17252882647"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ca6a9a9300d5bda417d9090107c6d2b007683efc59d63cc09aca0e7930a08a85"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6feb903d2a08a4ba2e70e950e862fd3be9be9a588b7c38cee4728150a52918"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b6f43f086e5a965d33d62a145ae121b4ccb6e0789ac0acc895ce084fec8c65"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:534fa66db8564d9b13872d81d54b6b09ae592c585eb826aac235bd6f1830f8ad"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fea649f979def23150680de1bd1d09682da3b54932800a0f90f29fc2a6c98ba8"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a447247ed312dd64e3a8d9483841ecc5338ee26d6e6fbd29cd373ed030db0240"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba3f843aa89f35467b38c398ae5b980a824fdbdb94065adc6ec7c47a0a22f4c7"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582c22f97a380211fb36a7b65b1beeb84ea11d82015fa84b054be78580390082"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47feb089506fc66e1593cd9ade3945693a9d089a445fbe9a11385cab200b9f22"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba9002d2f043943744a9dc8e50a47362bcb6e6f360dc0a1abcb19642584d87bb"}, - {file = "cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282"}, + {file = "cytoolz-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ecf5a887acb8f079ab1b81612b1c889bcbe6611aa7804fd2df46ed310aa5a345"}, + {file = "cytoolz-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0ef30c1e091d4d59d14d8108a16d50bd227be5d52a47da891da5019ac2f8e4"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7df2dfd679f0517a96ced1cdd22f5c6c6aeeed28d928a82a02bf4c3fd6fd7ac4"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c51452c938e610f57551aa96e34924169c9100c0448bac88c2fb395cbd3538c"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6433f03910c5e5345d82d6299457c26bf33821224ebb837c6b09d9cdbc414a6c"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:389ec328bb535f09e71dfe658bf0041f17194ca4cedaacd39bafe7893497a819"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64658e1209517ce4b54c1c9269a508b289d8d55fc742760e4b8579eacf09a33"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6039a9bd5bb988762458b9ca82b39e60ca5e5baae2ba93913990dcc5d19fa88"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85c9c8c4465ed1b2c8d67003809aec9627b129cb531d2f6cf0bbfe39952e7e4d"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:49375aad431d76650f94877afb92f09f58b6ff9055079ef4f2cd55313f5a1b39"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4c45106171c824a61e755355520b646cb35a1987b34bbf5789443823ee137f63"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3b319a7f0fed5db07d189db4046162ebc183c108df3562a65ba6ebe862d1f634"}, + {file = "cytoolz-1.0.0-cp310-cp310-win32.whl", hash = "sha256:9770e1b09748ad0d751853d994991e2592a9f8c464a87014365f80dac2e83faa"}, + {file = "cytoolz-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:20194dd02954c00c1f0755e636be75a20781f91a4ac9270c7f747e82d3c7f5a5"}, + {file = "cytoolz-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dffc22fd2c91be64dbdbc462d0786f8e8ac9a275cfa1869a1084d1867d4f67e0"}, + {file = "cytoolz-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a99e7e29274e293f4ffe20e07f76c2ac753a78f1b40c1828dfc54b2981b2f6c4"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c507a3e0a45c41d66b43f96797290d75d1e7a8549aa03a4a6b8854fdf3f7b8d8"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:643a593ec272ef7429099e1182a22f64ec2696c00d295d2a5be390db1b7ff176"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ce38e2e42cbae30446190c59b92a8a9029e1806fd79eaf88f48b0fe33003893"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810a6a168b8c5ecb412fbae3dd6f7ed6c6253a63caf4174ee9794ebd29b2224f"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ce8a2a85c0741c1b19b16e6782c4a5abc54c3caecda66793447112ab2fa9884"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea4ac72e6b830861035c4c7999af8e55813f57c6d1913a3d93cc4a6babc27bf7"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a09cdfb21dfb38aa04df43e7546a41f673377eb5485da88ceb784e327ec7603b"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:658dd85deb375ff7af990a674e5c9058cef1c9d1f5dc89bc87b77be499348144"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9715d1ff5576919d10b68f17241375f6a1eec8961c25b78a83e6ef1487053f39"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f370a1f1f1afc5c1c8cc5edc1cfe0ba444263a0772af7ce094be8e734f41769d"}, + {file = "cytoolz-1.0.0-cp311-cp311-win32.whl", hash = "sha256:dbb2ec1177dca700f3db2127e572da20de280c214fc587b2a11c717fc421af56"}, + {file = "cytoolz-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:0983eee73df86e54bb4a79fcc4996aa8b8368fdbf43897f02f9c3bf39c4dc4fb"}, + {file = "cytoolz-1.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e3986066dc379e30e225b230754d9f5996aa8d84c2accc69c473c21d261e46"}, + {file = "cytoolz-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:16576f1bb143ee2cb9f719fcc4b845879fb121f9075c7c5e8a5ff4854bd02fc6"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3faa25a1840b984315e8b3ae517312375f4273ffc9a2f035f548b7f916884f37"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781fce70a277b20fd95dc66811d1a97bb07b611ceea9bda8b7dd3c6a4b05d59a"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a562c25338eb24d419d1e80a7ae12133844ce6fdeb4ab54459daf250088a1b2"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f29d8330aaf070304f7cd5cb7e73e198753624eb0aec278557cccd460c699b5b"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98a96c54aa55ed9c7cdb23c2f0df39a7b4ee518ac54888480b5bdb5ef69c7ef0"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:287d6d7f475882c2ddcbedf8da9a9b37d85b77690779a2d1cdceb5ae3998d52e"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:05a871688df749b982839239fcd3f8ec3b3b4853775d575ff9cd335fa7c75035"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:28bb88e1e2f7d6d4b8e0890b06d292c568984d717de3e8381f2ca1dd12af6470"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:576a4f1fc73d8836b10458b583f915849da6e4f7914f4ecb623ad95c2508cad5"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:509ed3799c47e4ada14f63e41e8f540ac6e2dab97d5d7298934e6abb9d3830ec"}, + {file = "cytoolz-1.0.0-cp312-cp312-win32.whl", hash = "sha256:9ce25f02b910630f6dc2540dd1e26c9326027ddde6c59f8cab07c56acc70714c"}, + {file = "cytoolz-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e53cfcce87e05b7f0ae2fb2b3e5820048cd0bb7b701e92bd8f75c9fbb7c9ae9"}, + {file = "cytoolz-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d56569dfe67a39ce74ffff0dc12cf0a3d1aae709667a303fe8f2dd5fd004fdf"}, + {file = "cytoolz-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:035c8bb4706dcf93a89fb35feadff67e9301935bf6bb864cd2366923b69d9a29"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27c684799708bdc7ee7acfaf464836e1b4dec0996815c1d5efd6a92a4356a562"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44ab57cfc922b15d94899f980d76759ef9e0256912dfab70bf2561bea9cd5b19"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:478af5ecc066da093d7660b23d0b465a7f44179739937afbded8af00af412eb6"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da1f82a7828a42468ea2820a25b6e56461361390c29dcd4d68beccfa1b71066b"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c371b3114d38ee717780b239179e88d5d358fe759a00dcf07691b8922bbc762"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:90b343b2f3b3e77c3832ba19b0b17e95412a5b2e715b05c23a55ba525d1fca49"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89a554a9ba112403232a54e15e46ff218b33020f3f45c4baf6520ab198b7ad93"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0d603f5e2b1072166745ecdd81384a75757a96a704a5642231eb51969f919d5f"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:122ef2425bd3c0419e6e5260d0b18cd25cf74de589cd0184e4a63b24a4641e2e"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8819f1f97ebe36efcaf4b550e21677c46ac8a41bed482cf66845f377dd20700d"}, + {file = "cytoolz-1.0.0-cp38-cp38-win32.whl", hash = "sha256:fcddbb853770dd6e270d89ea8742f0aa42c255a274b9e1620eb04e019b79785e"}, + {file = "cytoolz-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:ca526905a014a38cc23ae78635dc51d0462c5c24425b22c08beed9ff2ee03845"}, + {file = "cytoolz-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05df5ff1cdd198fb57e7368623662578c950be0b14883cadfb9ee4098415e1e5"}, + {file = "cytoolz-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04a84778f48ebddb26948971dc60948907c876ba33b13f9cbb014fe65b341fc2"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f65283b618b4c4df759f57bcf8483865a73f7f268e6d76886c743407c8d26c1c"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388cd07ee9a9e504c735a0a933e53c98586a1c301a64af81f7aa7ff40c747520"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06d09e9569cfdfc5c082806d4b4582db8023a3ce034097008622bcbac7236f38"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9502bd9e37779cc9893cbab515a474c2ab6af61ed22ac2f7e16033db18fcaa85"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:364c2fda148def38003b2c86e8adde1d2aab12411dd50872c244a815262e2fda"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2e945617325242687189966335e785dc0fae316f4c1825baacf56e5a97e65f"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0f16907fdc724c55b16776bdb7e629deae81d500fe48cfc3861231753b271355"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d3206c81ca3ba2d7b8fe78f2e116e3028e721148be753308e88dcbbc370bca52"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:becce4b13e110b5ac6b23753dcd0c977f4fdccffa31898296e13fd1109e517e3"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69a7e5e98fd446079b8b8ec5987aec9a31ec3570a6f494baefa6800b783eaf22"}, + {file = "cytoolz-1.0.0-cp39-cp39-win32.whl", hash = "sha256:b1707b6c3a91676ac83a28a231a14b337dbb4436b937e6b3e4fd44209852a48b"}, + {file = "cytoolz-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:11d48b8521ef5fe92e099f4fc00717b5d0789c3c90d5d84031b6d3b17dee1700"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e672712d5dc3094afc6fb346dd4e9c18c1f3c69608ddb8cf3b9f8428f9c26a5c"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86fb208bfb7420e1d0d20065d661310e4a8a6884851d4044f47d37ed4cd7410e"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dbe5fe3b835859fc559eb59bf2775b5a108f7f2cfab0966f3202859d787d8fd"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cace092dfda174eed09ed871793beb5b65633963bcda5b1632c73a5aceea1ce"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f7a9d816af3be9725c70efe0a6e4352a45d3877751b395014b8eb2f79d7d8d9d"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:caa7ef840847a23b379e6146760e3a22f15f445656af97e55a435c592125cfa5"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921082fff09ff6e40c12c87b49be044492b2d6bb01d47783995813b76680c7b2"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a32f1356f3b64dda883583383966948604ac69ca0b7fbcf5f28856e5f9133b4e"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af793b1738e4191d15a92e1793f1ffea9f6461022c7b2442f3cb1ea0a4f758a"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:51dfda3983fcc59075c534ce54ca041bb3c80e827ada5d4f25ff7b4049777f94"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:acfb8780c04d29423d14aaab74cd1b7b4beaba32f676e7ace02c9acfbf532aba"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99f39dcc46416dca3eb23664b73187b77fb52cd8ba2ddd8020a292d8f449db67"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d56b3721977806dcf1a68b0ecd56feb382fdb0f632af1a9fc5ab9b662b32c6"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d346620abc8c83ae634136e700432ad6202faffcc24c5ab70b87392dcda8a1"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:df0c81197fc130de94c09fc6f024a6a19c98ba8fe55c17f1e45ebba2e9229079"}, + {file = "cytoolz-1.0.0.tar.gz", hash = "sha256:eb453b30182152f9917a5189b7d99046b6ce90cdf8aeb0feff4b2683e600defd"}, ] [package.dependencies] @@ -768,20 +765,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] -[[package]] -name = "dunamai" -version = "1.22.0" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -files = [ - {file = "dunamai-1.22.0-py3-none-any.whl", hash = "sha256:eab3894b31e145bd028a74b13491c57db01986a7510482c9b5fff3b4e53d77b7"}, - {file = "dunamai-1.22.0.tar.gz", hash = "sha256:375a0b21309336f0d8b6bbaea3e038c36f462318c68795166e31f9873fdad676"}, -] - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "et-xmlfile" version = "1.1.0" @@ -2028,68 +2011,6 @@ files = [ {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, ] -[[package]] -name = "numpy" -version = "2.1.1" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.10" -files = [ - {file = "numpy-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8a0e34993b510fc19b9a2ce7f31cb8e94ecf6e924a40c0c9dd4f62d0aac47d9"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:5889dd24f03ca5a5b1e8a90a33b5a0846d8977565e4ae003a63d22ecddf6782f"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:59ca673ad11d4b84ceb385290ed0ebe60266e356641428c845b39cd9df6713ab"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ce49a34c44b6de5241f0b38b07e44c1b2dcacd9e36c30f9c2fcb1bb5135db7"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:caf5d284ddea7462c32b8d4a6b8af030b6c9fd5332afb70e7414d7fdded4bfd0"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:57eb525e7c2a8fdee02d731f647146ff54ea8c973364f3b850069ffb42799647"}, - {file = "numpy-2.1.1-cp310-cp310-win32.whl", hash = "sha256:9a8e06c7a980869ea67bbf551283bbed2856915f0a792dc32dd0f9dd2fb56728"}, - {file = "numpy-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d10c39947a2d351d6d466b4ae83dad4c37cd6c3cdd6d5d0fa797da56f710a6ae"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d07841fd284718feffe7dd17a63a2e6c78679b2d386d3e82f44f0108c905550"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5613cfeb1adfe791e8e681128f5f49f22f3fcaa942255a6124d58ca59d9528f"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0b8cc2715a84b7c3b161f9ebbd942740aaed913584cae9cdc7f8ad5ad41943d0"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b49742cdb85f1f81e4dc1b39dcf328244f4d8d1ded95dea725b316bd2cf18c95"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d5f8a8e3bc87334f025194c6193e408903d21ebaeb10952264943a985066ca"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d51fc141ddbe3f919e91a096ec739f49d686df8af254b2053ba21a910ae518bf"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98ce7fb5b8063cfdd86596b9c762bf2b5e35a2cdd7e967494ab78a1fa7f8b86e"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24c2ad697bd8593887b019817ddd9974a7f429c14a5469d7fad413f28340a6d2"}, - {file = "numpy-2.1.1-cp311-cp311-win32.whl", hash = "sha256:397bc5ce62d3fb73f304bec332171535c187e0643e176a6e9421a6e3eacef06d"}, - {file = "numpy-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:ae8ce252404cdd4de56dcfce8b11eac3c594a9c16c231d081fb705cf23bd4d9e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c803b7934a7f59563db459292e6aa078bb38b7ab1446ca38dd138646a38203e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3269c9eb8745e8d975980b3a7411a98976824e1fdef11f0aacf76147f662b15f"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fac6e277a41163d27dfab5f4ec1f7a83fac94e170665a4a50191b545721c6521"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:afd9c680df4de71cd58582b51e88a61feed4abcc7530bcd3d48483f20fc76f2a"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8661c94e3aad18e1ea17a11f60f843a4933ccaf1a25a7c6a9182af70610b2313"}, - {file = "numpy-2.1.1-cp312-cp312-win32.whl", hash = "sha256:950802d17a33c07cba7fd7c3dcfa7d64705509206be1606f196d179e539111ed"}, - {file = "numpy-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fc5eabfc720db95d68e6646e88f8b399bfedd235994016351b1d9e062c4b270"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:046356b19d7ad1890c751b99acad5e82dc4a02232013bd9a9a712fddf8eb60f5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e5a9cb2be39350ae6c8f79410744e80154df658d5bea06e06e0ac5bb75480d5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d4c57b68c8ef5e1ebf47238e99bf27657511ec3f071c465f6b1bccbef12d4136"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8ae0fd135e0b157365ac7cc31fff27f07a5572bdfc38f9c2d43b2aff416cc8b0"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981707f6b31b59c0c24bcda52e5605f9701cb46da4b86c2e8023656ad3e833cb"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca4b53e1e0b279142113b8c5eb7d7a877e967c306edc34f3b58e9be12fda8df"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e097507396c0be4e547ff15b13dc3866f45f3680f789c1a1301b07dadd3fbc78"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7506387e191fe8cdb267f912469a3cccc538ab108471291636a96a54e599556"}, - {file = "numpy-2.1.1-cp313-cp313-win32.whl", hash = "sha256:251105b7c42abe40e3a689881e1793370cc9724ad50d64b30b358bbb3a97553b"}, - {file = "numpy-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:f212d4f46b67ff604d11fff7cc62d36b3e8714edf68e44e9760e19be38c03eb0"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:920b0911bb2e4414c50e55bd658baeb78281a47feeb064ab40c2b66ecba85553"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bab7c09454460a487e631ffc0c42057e3d8f2a9ddccd1e60c7bb8ed774992480"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:cea427d1350f3fd0d2818ce7350095c1a2ee33e30961d2f0fef48576ddbbe90f"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:e30356d530528a42eeba51420ae8bf6c6c09559051887196599d96ee5f536468"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8dfa9e94fc127c40979c3eacbae1e61fda4fe71d84869cc129e2721973231ef"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910b47a6d0635ec1bd53b88f86120a52bf56dcc27b51f18c7b4a2e2224c29f0f"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:13cc11c00000848702322af4de0147ced365c81d66053a67c2e962a485b3717c"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53e27293b3a2b661c03f79aa51c3987492bd4641ef933e366e0f9f6c9bf257ec"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7be6a07520b88214ea85d8ac8b7d6d8a1839b0b5cb87412ac9f49fa934eb15d5"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:52ac2e48f5ad847cd43c4755520a2317f3380213493b9d8a4c5e37f3b87df504"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a95ca3560a6058d6ea91d4629a83a897ee27c00630aed9d933dff191f170cd"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:99f4a9ee60eed1385a86e82288971a51e71df052ed0b2900ed30bc840c0f2e39"}, - {file = "numpy-2.1.1.tar.gz", hash = "sha256:d0cf7d55b1051387807405b3898efafa862997b4cba8aa5dbe657be794afeafd"}, -] - [[package]] name = "openpyxl" version = "3.1.3" @@ -2197,70 +2118,51 @@ files = [ [[package]] name = "pandas" -version = "2.0.3" +version = "1.5.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.8" files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, + {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, + {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, + {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, + {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, + {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, + {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, + {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, ] [package.dependencies] numpy = [ {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] -python-dateutil = ">=2.8.2" +python-dateutil = ">=2.8.1" pytz = ">=2020.1" -tzdata = ">=2022.1" [package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] +test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] [[package]] name = "parsimonious" @@ -2329,25 +2231,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "poetry-dynamic-versioning" -version = "1.4.1" -description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" -optional = false -python-versions = "<4.0,>=3.7" -files = [ - {file = "poetry_dynamic_versioning-1.4.1-py3-none-any.whl", hash = "sha256:44866ccbf869849d32baed4fc5fadf97f786180d8efa1719c88bf17a471bd663"}, - {file = "poetry_dynamic_versioning-1.4.1.tar.gz", hash = "sha256:21584d21ca405aa7d83d23d38372e3c11da664a8742995bdd517577e8676d0e1"}, -] - -[package.dependencies] -dunamai = ">=1.21.0,<2.0.0" -jinja2 = ">=2.11.1,<4" -tomlkit = ">=0.4" - -[package.extras] -plugin = ["poetry (>=1.2.0,<2.0.0)"] - [[package]] name = "pottery" version = "3.0.0" @@ -2364,6 +2247,113 @@ mmh3 = "*" redis = ">=4,<5" typing-extensions = "*" +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + [[package]] name = "protobuf" version = "5.28.2" @@ -2478,43 +2468,43 @@ files = [ [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, ] [[package]] @@ -2602,25 +2592,29 @@ files = [ [[package]] name = "pywin32" -version = "306" +version = "307" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b"}, + {file = "pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d"}, + {file = "pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4"}, + {file = "pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75"}, + {file = "pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3"}, + {file = "pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398"}, + {file = "pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815"}, + {file = "pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347"}, + {file = "pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2"}, + {file = "pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5"}, + {file = "pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2"}, + {file = "pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a"}, + {file = "pywin32-307-cp37-cp37m-win32.whl", hash = "sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511"}, + {file = "pywin32-307-cp37-cp37m-win_amd64.whl", hash = "sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba"}, + {file = "pywin32-307-cp38-cp38-win32.whl", hash = "sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01"}, + {file = "pywin32-307-cp38-cp38-win_amd64.whl", hash = "sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b"}, + {file = "pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6"}, + {file = "pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87"}, ] [[package]] @@ -2844,19 +2838,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.8.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -3141,26 +3135,15 @@ files = [ {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "toolz" -version = "0.12.1" +version = "1.0.0" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, ] [[package]] @@ -3194,17 +3177,6 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - [[package]] name = "urllib3" version = "2.2.3" @@ -3450,108 +3422,109 @@ files = [ [[package]] name = "yarl" -version = "1.12.1" +version = "1.14.0" description = "Yet another URL library" optional = false python-versions = ">=3.8" files = [ - {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc"}, - {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff"}, - {file = "yarl-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267"}, - {file = "yarl-1.12.1-cp310-cp310-win32.whl", hash = "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2"}, - {file = "yarl-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a"}, - {file = "yarl-1.12.1-cp311-cp311-win32.whl", hash = "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504"}, - {file = "yarl-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca"}, - {file = "yarl-1.12.1-cp312-cp312-win32.whl", hash = "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b"}, - {file = "yarl-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3"}, - {file = "yarl-1.12.1-cp313-cp313-win32.whl", hash = "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294"}, - {file = "yarl-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500"}, - {file = "yarl-1.12.1-cp38-cp38-win32.whl", hash = "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d"}, - {file = "yarl-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15"}, - {file = "yarl-1.12.1-cp39-cp39-win32.whl", hash = "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8"}, - {file = "yarl-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01"}, - {file = "yarl-1.12.1-py3-none-any.whl", hash = "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb"}, - {file = "yarl-1.12.1.tar.gz", hash = "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828"}, + {file = "yarl-1.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1bfc25aa6a7c99cf86564210f79a0b7d4484159c67e01232b116e445b3036547"}, + {file = "yarl-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0cf21f46a15d445417de8fc89f2568852cf57fe8ca1ab3d19ddb24d45c0383ae"}, + {file = "yarl-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1dda53508df0de87b6e6b0a52d6718ff6c62a5aca8f5552748404963df639269"}, + {file = "yarl-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:587c3cc59bc148a9b1c07a019346eda2549bc9f468acd2f9824d185749acf0a6"}, + {file = "yarl-1.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3007a5b75cb50140708420fe688c393e71139324df599434633019314ceb8b59"}, + {file = "yarl-1.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06ff23462398333c78b6f4f8d3d70410d657a471c2c5bbe6086133be43fc8f1a"}, + {file = "yarl-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689a99a42ee4583fcb0d3a67a0204664aa1539684aed72bdafcbd505197a91c4"}, + {file = "yarl-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0547ab1e9345dc468cac8368d88ea4c5bd473ebc1d8d755347d7401982b5dd8"}, + {file = "yarl-1.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:742aef0a99844faaac200564ea6f5e08facb285d37ea18bd1a5acf2771f3255a"}, + {file = "yarl-1.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:176110bff341b6730f64a1eb3a7070e12b373cf1c910a9337e7c3240497db76f"}, + {file = "yarl-1.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46a9772a1efa93f9cd170ad33101c1817c77e0e9914d4fe33e2da299d7cf0f9b"}, + {file = "yarl-1.14.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ee2c68e4f2dd1b1c15b849ba1c96fac105fca6ffdb7c1e8be51da6fabbdeafb9"}, + {file = "yarl-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:047b258e00b99091b6f90355521f026238c63bd76dcf996d93527bb13320eefd"}, + {file = "yarl-1.14.0-cp310-cp310-win32.whl", hash = "sha256:0aa92e3e30a04f9462a25077db689c4ac5ea9ab6cc68a2e563881b987d42f16d"}, + {file = "yarl-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:d9baec588f015d0ee564057aa7574313c53a530662ffad930b7886becc85abdf"}, + {file = "yarl-1.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:07f9eaf57719d6721ab15805d85f4b01a5b509a0868d7320134371bcb652152d"}, + {file = "yarl-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c14b504a74e58e2deb0378b3eca10f3d076635c100f45b113c18c770b4a47a50"}, + {file = "yarl-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a682a127930f3fc4e42583becca6049e1d7214bcad23520c590edd741d2114"}, + {file = "yarl-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73bedd2be05f48af19f0f2e9e1353921ce0c83f4a1c9e8556ecdcf1f1eae4892"}, + {file = "yarl-1.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3ab950f8814f3b7b5e3eebc117986f817ec933676f68f0a6c5b2137dd7c9c69"}, + {file = "yarl-1.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b693c63e7e64b524f54aa4888403c680342d1ad0d97be1707c531584d6aeeb4f"}, + {file = "yarl-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cb3e40eaa98489f1e2e8b29f5ad02ee1ee40d6ce6b88d50cf0f205de1d9d2c"}, + {file = "yarl-1.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f24f08b6c9b9818fd80612c97857d28f9779f0d1211653ece9844fc7b414df2"}, + {file = "yarl-1.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29a84a46ec3ebae7a1c024c055612b11e9363a8a23238b3e905552d77a2bc51b"}, + {file = "yarl-1.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5cd5dad8366e0168e0fd23d10705a603790484a6dbb9eb272b33673b8f2cce72"}, + {file = "yarl-1.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a152751af7ef7b5d5fa6d215756e508dd05eb07d0cf2ba51f3e740076aa74373"}, + {file = "yarl-1.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3d569f877ed9a708e4c71a2d13d2940cb0791da309f70bd970ac1a5c088a0a92"}, + {file = "yarl-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a615cad11ec3428020fb3c5a88d85ce1b5c69fd66e9fcb91a7daa5e855325dd"}, + {file = "yarl-1.14.0-cp311-cp311-win32.whl", hash = "sha256:bab03192091681d54e8225c53f270b0517637915d9297028409a2a5114ff4634"}, + {file = "yarl-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:985623575e5c4ea763056ffe0e2d63836f771a8c294b3de06d09480538316b13"}, + {file = "yarl-1.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fc2c80bc87fba076e6cbb926216c27fba274dae7100a7b9a0983b53132dd99f2"}, + {file = "yarl-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:55c144d363ad4626ca744556c049c94e2b95096041ac87098bb363dcc8635e8d"}, + {file = "yarl-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b03384eed107dbeb5f625a99dc3a7de8be04fc8480c9ad42fccbc73434170b20"}, + {file = "yarl-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f72a0d746d38cb299b79ce3d4d60ba0892c84bbc905d0d49c13df5bace1b65f8"}, + {file = "yarl-1.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8648180b34faaea4aa5b5ca7e871d9eb1277033fa439693855cf0ea9195f85f1"}, + {file = "yarl-1.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9557c9322aaa33174d285b0c1961fb32499d65ad1866155b7845edc876c3c835"}, + {file = "yarl-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f50eb3837012a937a2b649ec872b66ba9541ad9d6f103ddcafb8231cfcafd22"}, + {file = "yarl-1.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8892fa575ac9b1b25fae7b221bc4792a273877b9b56a99ee2d8d03eeb3dbb1d2"}, + {file = "yarl-1.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6a2c5c5bb2556dfbfffffc2bcfb9c235fd2b566d5006dfb2a37afc7e3278a07"}, + {file = "yarl-1.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ab3abc0b78a5dfaa4795a6afbe7b282b6aa88d81cf8c1bb5e394993d7cae3457"}, + {file = "yarl-1.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:47eede5d11d669ab3759b63afb70d28d5328c14744b8edba3323e27dc52d298d"}, + {file = "yarl-1.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fe4d2536c827f508348d7b40c08767e8c7071614250927233bf0c92170451c0a"}, + {file = "yarl-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0fd7b941dd1b00b5f0acb97455fea2c4b7aac2dd31ea43fb9d155e9bc7b78664"}, + {file = "yarl-1.14.0-cp312-cp312-win32.whl", hash = "sha256:99ff3744f5fe48288be6bc402533b38e89749623a43208e1d57091fc96b783b9"}, + {file = "yarl-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ca3894e9e9f72da93544f64988d9c052254a338a9f855165f37f51edb6591de"}, + {file = "yarl-1.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d02d700705d67e09e1f57681f758f0b9d4412eeb70b2eb8d96ca6200b486db3"}, + {file = "yarl-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:30600ba5db60f7c0820ef38a2568bb7379e1418ecc947a0f76fd8b2ff4257a97"}, + {file = "yarl-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e85d86527baebb41a214cc3b45c17177177d900a2ad5783dbe6f291642d4906f"}, + {file = "yarl-1.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37001e5d4621cef710c8dc1429ca04e189e572f128ab12312eab4e04cf007132"}, + {file = "yarl-1.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4f4547944d4f5cfcdc03f3f097d6f05bbbc915eaaf80a2ee120d0e756de377d"}, + {file = "yarl-1.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ff4c819757f9bdb35de049a509814d6ce851fe26f06eb95a392a5640052482"}, + {file = "yarl-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68ac1a09392ed6e3fd14be880d39b951d7b981fd135416db7d18a6208c536561"}, + {file = "yarl-1.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96952f642ac69075e44c7d0284528938fdff39422a1d90d3e45ce40b72e5e2d9"}, + {file = "yarl-1.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a56fbe3d7f3bce1d060ea18d2413a2ca9ca814eea7cedc4d247b5f338d54844e"}, + {file = "yarl-1.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e2637d75e92763d1322cb5041573279ec43a80c0f7fbbd2d64f5aee98447b17"}, + {file = "yarl-1.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9abe80ae2c9d37c17599557b712e6515f4100a80efb2cda15f5f070306477cd2"}, + {file = "yarl-1.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:217a782020b875538eebf3948fac3a7f9bbbd0fd9bf8538f7c2ad7489e80f4e8"}, + {file = "yarl-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9cfef3f14f75bf6aba73a76caf61f9d00865912a04a4393c468a7ce0981b519"}, + {file = "yarl-1.14.0-cp313-cp313-win32.whl", hash = "sha256:d8361c7d04e6a264481f0b802e395f647cd3f8bbe27acfa7c12049efea675bd1"}, + {file = "yarl-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:bc24f968b82455f336b79bf37dbb243b7d76cd40897489888d663d4e028f5069"}, + {file = "yarl-1.14.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:91d875f75fabf76b3018c5f196bf3d308ed2b49ddcb46c1576d6b075754a1393"}, + {file = "yarl-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4009def9be3a7e5175db20aa2d7307ecd00bbf50f7f0f989300710eee1d0b0b9"}, + {file = "yarl-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:582cedde49603f139be572252a318b30dc41039bc0b8165f070f279e5d12187f"}, + {file = "yarl-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbd9ff43a04f8ffe8a959a944c2dca10d22f5f99fc6a459f49c3ebfb409309d9"}, + {file = "yarl-1.14.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f805e37ed16cc212fdc538a608422d7517e7faf539bedea4fe69425bc55d76"}, + {file = "yarl-1.14.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95e16e9eaa2d7f5d87421b8fe694dd71606aa61d74b824c8d17fc85cc51983d1"}, + {file = "yarl-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:816d24f584edefcc5ca63428f0b38fee00b39fe64e3c5e558f895a18983efe96"}, + {file = "yarl-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd2660c01367eb3ef081b8fa0a5da7fe767f9427aa82023a961a5f28f0d4af6c"}, + {file = "yarl-1.14.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:94b2bb9bcfd5be9d27004ea4398fb640373dd0c1a9e219084f42c08f77a720ab"}, + {file = "yarl-1.14.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c2089a9afef887664115f7fa6d3c0edd6454adaca5488dba836ca91f60401075"}, + {file = "yarl-1.14.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2192f718db4a8509f63dd6d950f143279211fa7e6a2c612edc17d85bf043d36e"}, + {file = "yarl-1.14.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:8385ab36bf812e9d37cf7613999a87715f27ef67a53f0687d28c44b819df7cb0"}, + {file = "yarl-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b4c1ecba93e7826dc71ddba75fb7740cdb52e7bd0be9f03136b83f54e6a1f511"}, + {file = "yarl-1.14.0-cp38-cp38-win32.whl", hash = "sha256:e749af6c912a7bb441d105c50c1a3da720474e8acb91c89350080dd600228f0e"}, + {file = "yarl-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:147e36331f6f63e08a14640acf12369e041e0751bb70d9362df68c2d9dcf0c87"}, + {file = "yarl-1.14.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a9f917966d27f7ce30039fe8d900f913c5304134096554fd9bea0774bcda6d1"}, + {file = "yarl-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a2f8fb7f944bcdfecd4e8d855f84c703804a594da5123dd206f75036e536d4d"}, + {file = "yarl-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f4e475f29a9122f908d0f1f706e1f2fc3656536ffd21014ff8a6f2e1b14d1d8"}, + {file = "yarl-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8089d4634d8fa2b1806ce44fefa4979b1ab2c12c0bc7ef3dfa45c8a374811348"}, + {file = "yarl-1.14.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b16f6c75cffc2dc0616ea295abb0e1967601bd1fb1e0af6a1de1c6c887f3439"}, + {file = "yarl-1.14.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498b3c55087b9d762636bca9b45f60d37e51d24341786dc01b81253f9552a607"}, + {file = "yarl-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f8bfc1db82589ef965ed234b87de30d140db8b6dc50ada9e33951ccd8ec07a"}, + {file = "yarl-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:625f207b1799e95e7c823f42f473c1e9dbfb6192bd56bba8695656d92be4535f"}, + {file = "yarl-1.14.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:781e2495e408a81e4eaeedeb41ba32b63b1980dddf8b60dbbeff6036bcd35049"}, + {file = "yarl-1.14.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:659603d26d40dd4463200df9bfbc339fbfaed3fe32e5c432fe1dc2b5d4aa94b4"}, + {file = "yarl-1.14.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4e0d45ebf975634468682c8bec021618b3ad52c37619e5c938f8f831fa1ac5c0"}, + {file = "yarl-1.14.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a2e4725a08cb2b4794db09e350c86dee18202bb8286527210e13a1514dc9a59a"}, + {file = "yarl-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:19268b4fec1d7760134f2de46ef2608c2920134fb1fa61e451f679e41356dc55"}, + {file = "yarl-1.14.0-cp39-cp39-win32.whl", hash = "sha256:337912bcdcf193ade64b9aae5a4017a0a1950caf8ca140362e361543c6773f21"}, + {file = "yarl-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:b6d0147574ce2e7b812c989e50fa72bbc5338045411a836bd066ce5fc8ac0bce"}, + {file = "yarl-1.14.0-py3-none-any.whl", hash = "sha256:c8ed4034f0765f8861620c1f2f2364d2e58520ea288497084dae880424fc0d9f"}, + {file = "yarl-1.14.0.tar.gz", hash = "sha256:88c7d9d58aab0724b979ab5617330acb1c7030b79379c8138c1c8c94e121d1b3"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +propcache = ">=0.2.0" [[package]] name = "zipp" @@ -3575,4 +3548,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4" -content-hash = "c6d2b4d641ecbe9fac79477c371e11635b10c70e6f6d74ea6ad271de6e04491a" +content-hash = "4babf902cda2c8ab2f6fe0ffc4aec27f3e38d90fab822bfb1d9b4b5901c20d16" diff --git a/pyproject.toml b/pyproject.toml index 8af2937d6..c8ce45f55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry_dynamic_versioning.backend" [tool.poetry] name = "hemera" description = "Tools for exporting Ethereum blockchain data to JSON/CSV file and postgresql" -version = "0.3.0" +version = "0.4.0" authors = [ "xuzh ", "shanshuo0918 " @@ -33,7 +33,7 @@ packages = [ [tool.poetry.dependencies] python = ">=3.8,<4" -web3 = ">=6.8.0,<7" +web3 = "6.20.3" eth-utils = ">=4.0.0" eth-abi = ">=5.0.1" python-dateutil = ">=2.8.0,<3" @@ -43,7 +43,7 @@ requests = "*" sqlalchemy = "2.0.31" psycopg2-binary = "2.9.9" alembic = "*" -pandas = "*" +pandas = "1.5.3" Flask = "3.0.3" Flask-Caching = "2.3.0" Flask-Cors = "3.0.9" @@ -61,7 +61,7 @@ eth_typing = ">=2.2.0,<5" orjson = "3.10.7" mpire = "2.10.2" PyYAML = "6.0.2" -poetry_dynamic_versioning = ">=1.2.0" +numpy = "1.24.4" [tool.poetry.group.dev.dependencies] pytest = ">=7.0.0" @@ -80,7 +80,7 @@ hemera = "cli:cli" [tool.black] line-length = 120 -target-version = ["py38", "py39", "py310", "py311", "py312"] +target-version = ["py38", "py39", "py310", "py311"] [tool.isort] profile = "black" From 54d471951e93dc8a3340bb825cc8af6dc68fc55d Mon Sep 17 00:00:00 2001 From: xuzh2024 <167734725+xuzh2024@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:49:21 +0800 Subject: [PATCH 06/12] change param --retry-from-record default value (#181) --- cli/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/stream.py b/cli/stream.py index 8c020ddfc..10dd5bff6 100644 --- a/cli/stream.py +++ b/cli/stream.py @@ -129,7 +129,7 @@ def wrapper(*args, **kwargs): ) @click.option( "--retry-from-record", - default=False, + default=True, show_default=True, type=bool, envvar="RETRY_FROM_RECORD", From 9d36df36f0c71e99ee21e3daf9ec950c4eb8aa2c Mon Sep 17 00:00:00 2001 From: xuzh2024 <167734725+xuzh2024@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:39:06 +0800 Subject: [PATCH 07/12] Feature/add pg source (#159) * new feature: read data from postgres * new feature: pg_source_job can use filter to specify required data * bug fixed and performance optimized * make format * fix dict_to_dataclass error * fix iterator error * fix iterator error * merged cli load and stream & add limit reader & add param --source-type * optimize code, set default value to --source-types * optimize code logic, to prevent unexpected error * add partition key to enhance query performance. * bug fixed * fixed data convert error * Changes the sort operation from the database to memory * add logger info * fix rpc_limit_reader & parameter required * fix filter logic * fix filter logic * make format * resolved conflict * resolved conflict * make format * deal conflict * separate filter query * Optimize query filter order * Optimize code --- cli/__init__.py | 2 - cli/load.py | 329 ------------ cli/stream.py | 56 +- common/models/__init__.py | 2 + common/models/blocks.py | 2 + .../models/contract_internal_transactions.py | 2 + common/models/erc1155_token_transfers.py | 1 + common/models/erc20_token_transfers.py | 1 + common/models/erc721_token_transfers.py | 1 + common/models/logs.py | 1 + common/models/traces.py | 2 + common/models/transactions.py | 2 + common/services/postgresql_service.py | 44 +- common/utils/format_utils.py | 4 + indexer/controller/scheduler/job_scheduler.py | 38 +- indexer/controller/stream_controller.py | 14 +- indexer/domain/__init__.py | 8 +- indexer/domain/receipt.py | 20 + indexer/jobs/__init__.py | 4 +- indexer/jobs/export_blocks_job.py | 12 +- indexer/jobs/source_job/__init__.py | 0 .../jobs/{ => source_job}/csv_source_job.py | 0 indexer/jobs/source_job/pg_source_job.py | 500 ++++++++++++++++++ indexer/specification/specification.py | 18 + indexer/utils/limit_reader.py | 49 ++ indexer/utils/parameter_utils.py | 36 +- indexer/utils/utils.py | 12 +- 27 files changed, 773 insertions(+), 387 deletions(-) delete mode 100644 cli/load.py create mode 100644 indexer/jobs/source_job/__init__.py rename indexer/jobs/{ => source_job}/csv_source_job.py (100%) create mode 100644 indexer/jobs/source_job/pg_source_job.py create mode 100644 indexer/utils/limit_reader.py diff --git a/cli/__init__.py b/cli/__init__.py index 21a4449f4..fefee7f9d 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -5,7 +5,6 @@ from cli.aggregates import aggregates from cli.api import api from cli.init_db import init_db -from cli.load import load from cli.reorg import reorg from cli.stream import stream from indexer.utils.logging_utils import logging_basic_config @@ -27,7 +26,6 @@ def cli(ctx): cli.add_command(stream, "stream") -cli.add_command(load, "load") cli.add_command(api, "api") cli.add_command(aggregates, "aggregates") cli.add_command(reorg, "reorg") diff --git a/cli/load.py b/cli/load.py deleted file mode 100644 index 313bdc563..000000000 --- a/cli/load.py +++ /dev/null @@ -1,329 +0,0 @@ -import logging -import time - -import click - -from common.services.postgresql_service import PostgreSQLService -from enumeration.entity_type import DEFAULT_COLLECTION, calculate_entity_value, generate_output_types -from indexer.controller.scheduler.job_scheduler import JobScheduler -from indexer.controller.stream_controller import StreamController -from indexer.domain import Domain -from indexer.exporters.item_exporter import create_item_exporters -from indexer.utils.exception_recorder import ExceptionRecorder -from indexer.utils.logging_utils import configure_logging, configure_signals -from indexer.utils.parameter_utils import check_file_exporter_parameter, check_file_load_parameter -from indexer.utils.provider import get_provider_from_uri -from indexer.utils.sync_recorder import create_recorder -from indexer.utils.thread_local_proxy import ThreadLocalProxy -from indexer.utils.utils import pick_random_provider_uri - -exception_recorder = ExceptionRecorder() - - -def calculate_execution_time(func): - def wrapper(*args, **kwargs): - start_time = time.time() - result = func(*args, **kwargs) - end_time = time.time() - execution_time = end_time - start_time - print(f"function {func.__name__} time: {execution_time:.6f} s") - return result - - return wrapper - - -@click.command(context_settings=dict(help_option_names=["-h", "--help"])) -@click.option( - "-p", - "--provider-uri", - default="https://mainnet.infura.io", - show_default=True, - type=str, - envvar="PROVIDER_URI", - help="The URI of the web3 provider e.g. " "file://$HOME/Library/Ethereum/geth.ipc or https://mainnet.infura.io", -) -@click.option( - "-pg", - "--postgres-url", - type=str, - required=False, - envvar="POSTGRES_URL", - help="The required postgres connection url." "e.g. postgresql+psycopg2://postgres:admin@127.0.0.1:5432/ethereum", -) -@click.option( - "-d", - "--debug-provider-uri", - default="https://mainnet.infura.io", - show_default=True, - type=str, - envvar="DEBUG_PROVIDER_URI", - help="The URI of the web3 debug provider e.g. " - "file://$HOME/Library/Ethereum/geth.ipc or https://mainnet.infura.io", -) -@click.option( - "-o", - "--output", - type=str, - envvar="OUTPUT", - help="The output selection." - "Print to console e.g. console; " - "or postgresql e.g. postgres" - "or local json file e.g. jsonfile://your-file-path; " - "or local csv file e.g. csvfile://your-file-path; " - "or both. e.g. console,jsonfile://your-file-path,csvfile://your-file-path", -) -@click.option( - "-E", - "--entity-types", - default=",".join(DEFAULT_COLLECTION), - show_default=True, - type=str, - envvar="ENTITY_TYPES", - help="The list of entity types to export. " "e.g. EXPLORER_BASE | EXPLORER_TOKEN | EXPLORER_TRACE", -) -@click.option( - "-O", - "--output-types", - default=None, - show_default=True, - type=str, - envvar="OUTPUT_TYPES", - help="The list of output types to export, corresponding to more detailed data models. " - "Specifying this option will prioritize these settings over the entity types specified in -E. " - "Examples include: block, transaction, log, " - "token, address_token_balance, erc20_token_transfer, erc721_token_transfer, erc1155_token_transfer, " - "trace, contract, coin_balance.", -) -@click.option( - "-v", - "--db-version", - default="head", - show_default=True, - type=str, - envvar="DB_VERSION", - help="The database version to initialize the database. using the alembic script's revision ID to " - "specify a version. " - "e.g. head, indicates the latest version." - "or base, indicates the empty database without any table.", -) -@click.option( - "-s", - "--start-block", - default=None, - required=True, - show_default=True, - type=int, - help="Start block", - envvar="START_BLOCK", -) -@click.option( - "-e", - "--end-block", - default=None, - required=True, - show_default=True, - type=int, - help="End block", - envvar="END_BLOCK", -) -@click.option( - "--blocks-per-file", - default=1000, - show_default=True, - type=int, - envvar="BLOCKS_PER_FILE", - help="How many blocks data was written to each file", -) -@click.option( - "--period-seconds", - default=10, - show_default=True, - type=int, - envvar="PERIOD_SECONDS", - help="How many seconds to sleep between syncs", -) -@click.option( - "-b", - "--batch-size", - default=10, - show_default=True, - type=int, - envvar="BATCH_SIZE", - help="The number of non-debug RPC requests to batch in a single request", -) -@click.option( - "--debug-batch-size", - default=1, - show_default=True, - type=int, - envvar="DEBUG_BATCH_SIZE", - help="The number of debug RPC requests to batch in a single request", -) -@click.option( - "-B", - "--block-batch-size", - default=1, - show_default=True, - type=int, - envvar="BLOCK_BATCH_SIZE", - help="How many blocks to batch in single sync round", -) -@click.option( - "-w", - "--max-workers", - default=5, - show_default=True, - type=int, - help="The number of workers", - envvar="MAX_WORKERS", -) -@click.option( - "--source-path", - default=None, - show_default=True, - required=True, - type=str, - envvar="SOURCE_PATH", - help="The path to load the data." - "Load from local csv file e.g. csvfile://your-file-direction; " - "or local json file e.g. jsonfile://your-file-direction; ", -) -@click.option( - "--log-file", - default=None, - show_default=True, - type=str, - envvar="LOG_FILE", - help="Log file", -) -@click.option( - "--pid-file", - default=None, - show_default=True, - type=str, - envvar="PID_FILE", - help="Pid file", -) -@click.option( - "--sync-recorder", - default="file_sync_record", - show_default=True, - type=str, - envvar="SYNC_RECORDER", - help="How to store the sync record data." - 'e.g pg_base. means sync record data will store in pg as "base" be key' - 'or file_base. means sync record data will store in file as "base" be file name', -) -@click.option( - "--cache", - default="memory", - show_default=True, - type=str, - envvar="CACHE_SERVICE", - help="How to store the cache data." - "e.g redis. means cache data will store in redis, redis://localhost:6379" - "or memory. means cache data will store in memory, memory", -) -@click.option( - "--auto-upgrade-db", - default=True, - show_default=True, - type=bool, - envvar="AUTO_UPGRADE_DB", - help="Whether to automatically run database migration scripts to update the database to the latest version.", -) -@click.option( - "--log-level", - default="INFO", - show_default=True, - type=str, - envvar="LOG_LEVEL", - help="Set the logging output level.", -) -@calculate_execution_time -def load( - provider_uri, - debug_provider_uri, - postgres_url, - output, - db_version, - start_block, - end_block, - entity_types, - output_types, - blocks_per_file, - period_seconds=10, - batch_size=10, - debug_batch_size=1, - block_batch_size=1, - max_workers=5, - log_file=None, - pid_file=None, - source_path=None, - sync_recorder="file_sync_record", - cache="memory", - auto_upgrade_db=True, - log_level="INFO", -): - configure_logging(log_level=log_level, log_file=log_file) - configure_signals() - provider_uri = pick_random_provider_uri(provider_uri) - debug_provider_uri = pick_random_provider_uri(debug_provider_uri) - logging.info("Using provider " + provider_uri) - logging.info("Using debug provider " + debug_provider_uri) - - # parameter logic checking - check_file_load_parameter(source_path) - check_file_exporter_parameter(output, block_batch_size, blocks_per_file) - - # build config - config = {"blocks_per_file": blocks_per_file, "source_path": source_path} - - if postgres_url: - service = PostgreSQLService(postgres_url, db_version=db_version, init_schema=auto_upgrade_db) - config["db_service"] = service - exception_recorder.init_pg_service(service) - else: - logging.warning("No postgres url provided. Exception recorder will not be useful.") - - if output_types is None: - entity_types = calculate_entity_value(entity_types) - output_types = list(generate_output_types(entity_types)) - else: - domain_dict = Domain.get_all_domain_dict() - parse_output_types = set() - - for output_type in output_types.split(","): - if output_type not in domain_dict: - raise click.ClickException(f"Output type {output_type} is not supported") - parse_output_types.add(domain_dict[output_type]) - - if not output_types: - raise click.ClickException("No output types provided") - output_types = list(parse_output_types) - - job_scheduler = JobScheduler( - batch_web3_provider=ThreadLocalProxy(lambda: get_provider_from_uri(provider_uri, batch=True)), - batch_web3_debug_provider=ThreadLocalProxy(lambda: get_provider_from_uri(debug_provider_uri, batch=True)), - item_exporters=create_item_exporters(output, config), - batch_size=batch_size, - debug_batch_size=debug_batch_size, - max_workers=max_workers, - config=config, - required_output_types=output_types, - cache=cache, - ) - - controller = StreamController( - batch_web3_provider=ThreadLocalProxy(lambda: get_provider_from_uri(provider_uri, batch=False)), - job_scheduler=job_scheduler, - sync_recorder=create_recorder(sync_recorder, config), - ) - - controller.action( - start_block=start_block, - end_block=end_block, - block_batch_size=block_batch_size, - period_seconds=period_seconds, - pid_file=pid_file, - ) diff --git a/cli/stream.py b/cli/stream.py index 10dd5bff6..b52376211 100644 --- a/cli/stream.py +++ b/cli/stream.py @@ -6,15 +6,18 @@ from web3 import Web3 from common.services.postgresql_service import PostgreSQLService -from common.utils.format_utils import to_snake_case from enumeration.entity_type import DEFAULT_COLLECTION, calculate_entity_value, generate_output_types from indexer.controller.scheduler.job_scheduler import JobScheduler from indexer.controller.stream_controller import StreamController -from indexer.domain import Domain from indexer.exporters.item_exporter import create_item_exporters from indexer.utils.exception_recorder import ExceptionRecorder +from indexer.utils.limit_reader import create_limit_reader from indexer.utils.logging_utils import configure_logging, configure_signals -from indexer.utils.parameter_utils import check_file_exporter_parameter +from indexer.utils.parameter_utils import ( + check_file_exporter_parameter, + check_source_load_parameter, + generate_dataclass_type_list_from_parameter, +) from indexer.utils.provider import get_provider_from_uri from indexer.utils.sync_recorder import create_recorder from indexer.utils.thread_local_proxy import ThreadLocalProxy @@ -195,6 +198,28 @@ def wrapper(*args, **kwargs): envvar="DELAY", help="The limit number of blocks which delays from the network current block number.", ) +@click.option( + "--source-path", + default=None, + show_default=True, + required=False, + type=str, + envvar="SOURCE_PATH", + help="The path to load the data." + "Load from local csv file e.g. csvfile://your-file-direction; " + "or local json file e.g. jsonfile://your-file-direction; ", +) +@click.option( + "--source-types", + default="block,transaction,log", + show_default=True, + type=str, + envvar="SOURCE_TYPES", + help="The list of types to read from source, corresponding to more detailed data models. " + "Examples include: block, transaction, log, " + "token, address_token_balance, erc20_token_transfer, erc721_token_transfer, erc1155_token_transfer, " + "trace, contract, coin_balance.", +) @click.option( "--log-file", default=None, @@ -291,6 +316,7 @@ def stream( end_block, entity_types, output_types, + source_types, blocks_per_file, delay=0, period_seconds=10, @@ -300,10 +326,11 @@ def stream( max_workers=5, log_file=None, pid_file=None, + source_path=None, sync_recorder="file:sync_record", retry_from_record=False, cache="memory", - auto_reorg=True, + auto_reorg=False, multicall=True, config_file=None, force_filter_mode=False, @@ -318,11 +345,14 @@ def stream( logging.info("Using debug provider " + debug_provider_uri) # parameter logic checking + if source_path: + check_source_load_parameter(source_path, start_block, end_block, auto_reorg) check_file_exporter_parameter(output, block_batch_size, blocks_per_file) # build config config = { "blocks_per_file": blocks_per_file, + "source_path": source_path, "chain_id": Web3(Web3.HTTPProvider(provider_uri)).eth.chain_id, } @@ -361,18 +391,10 @@ def stream( entity_types = calculate_entity_value(entity_types) output_types = list(set(generate_output_types(entity_types))) else: - domain_dict = Domain.get_all_domain_dict() - parse_output_types = set() - - for output_type in output_types.split(","): - output_type = to_snake_case(output_type) - if output_type not in domain_dict: - raise click.ClickException(f"Output type {output_type} is not supported") - parse_output_types.add(domain_dict[output_type]) + output_types = generate_dataclass_type_list_from_parameter(output_types, "output") - if not output_types: - raise click.ClickException("No output types provided") - output_types = list(set(parse_output_types)) + if source_path and source_path.startswith("postgresql://"): + source_types = generate_dataclass_type_list_from_parameter(source_types, "source") job_scheduler = JobScheduler( batch_web3_provider=ThreadLocalProxy(lambda: get_provider_from_uri(provider_uri, batch=True)), @@ -383,6 +405,7 @@ def stream( max_workers=max_workers, config=config, required_output_types=output_types, + required_source_types=source_types, cache=cache, auto_reorg=auto_reorg, multicall=multicall, @@ -393,6 +416,9 @@ def stream( batch_web3_provider=ThreadLocalProxy(lambda: get_provider_from_uri(provider_uri, batch=False)), job_scheduler=job_scheduler, sync_recorder=create_recorder(sync_recorder, config), + limit_reader=create_limit_reader( + source_path, ThreadLocalProxy(lambda: get_provider_from_uri(provider_uri, batch=False)) + ), retry_from_record=retry_from_record, delay=delay, ) diff --git a/common/models/__init__.py b/common/models/__init__.py index 02f10e184..7944ace47 100644 --- a/common/models/__init__.py +++ b/common/models/__init__.py @@ -25,6 +25,8 @@ class HemeraModel(db.Model): __abstract__ = True + __query_order__ = [] + @staticmethod def model_domain_mapping(): pass diff --git a/common/models/blocks.py b/common/models/blocks.py index 9980e90a3..0bf11e43c 100644 --- a/common/models/blocks.py +++ b/common/models/blocks.py @@ -42,6 +42,8 @@ class Blocks(HemeraModel): update_time = Column(TIMESTAMP, server_default=func.now()) reorg = Column(BOOLEAN, server_default=text("false")) + __query_order__ = [number] + @staticmethod def model_domain_mapping(): return [ diff --git a/common/models/contract_internal_transactions.py b/common/models/contract_internal_transactions.py index 137638b06..005af9e07 100644 --- a/common/models/contract_internal_transactions.py +++ b/common/models/contract_internal_transactions.py @@ -30,6 +30,8 @@ class ContractInternalTransactions(HemeraModel): update_time = Column(TIMESTAMP, server_default=func.now()) reorg = Column(BOOLEAN, server_default=text("false")) + __query_order__ = [block_number, transaction_index] + @staticmethod def model_domain_mapping(): return [ diff --git a/common/models/erc1155_token_transfers.py b/common/models/erc1155_token_transfers.py index c7d529f80..7aa01c4ca 100644 --- a/common/models/erc1155_token_transfers.py +++ b/common/models/erc1155_token_transfers.py @@ -26,6 +26,7 @@ class ERC1155TokenTransfers(HemeraModel): reorg = Column(BOOLEAN, server_default=text("false")) __table_args__ = (PrimaryKeyConstraint("transaction_hash", "block_hash", "log_index", "token_id"),) + __query_order__ = [block_number, log_index] @staticmethod def model_domain_mapping(): diff --git a/common/models/erc20_token_transfers.py b/common/models/erc20_token_transfers.py index 83cc811c1..5455b04b8 100644 --- a/common/models/erc20_token_transfers.py +++ b/common/models/erc20_token_transfers.py @@ -25,6 +25,7 @@ class ERC20TokenTransfers(HemeraModel): reorg = Column(BOOLEAN, server_default=text("false")) __table_args__ = (PrimaryKeyConstraint("transaction_hash", "block_hash", "log_index"),) + __query_order__ = [block_number, log_index] @staticmethod def model_domain_mapping(): diff --git a/common/models/erc721_token_transfers.py b/common/models/erc721_token_transfers.py index 158990c99..74d539f42 100644 --- a/common/models/erc721_token_transfers.py +++ b/common/models/erc721_token_transfers.py @@ -25,6 +25,7 @@ class ERC721TokenTransfers(HemeraModel): reorg = Column(BOOLEAN, server_default=text("false")) __table_args__ = (PrimaryKeyConstraint("transaction_hash", "block_hash", "log_index"),) + __query_order__ = [block_number, log_index] @staticmethod def model_domain_mapping(): diff --git a/common/models/logs.py b/common/models/logs.py index ba1d2ba47..3870e187c 100644 --- a/common/models/logs.py +++ b/common/models/logs.py @@ -29,6 +29,7 @@ class Logs(HemeraModel): reorg = Column(BOOLEAN, server_default=text("false")) __table_args__ = (PrimaryKeyConstraint("transaction_hash", "block_hash", "log_index"),) + __query_order__ = [block_number, log_index] @staticmethod def model_domain_mapping(): diff --git a/common/models/traces.py b/common/models/traces.py index b2531167a..896a2ce46 100644 --- a/common/models/traces.py +++ b/common/models/traces.py @@ -33,6 +33,8 @@ class Traces(HemeraModel): update_time = Column(TIMESTAMP, server_default=func.now()) reorg = Column(BOOLEAN, server_default=text("false")) + __query_order__ = [block_number, transaction_index] + @staticmethod def model_domain_mapping(): return [ diff --git a/common/models/transactions.py b/common/models/transactions.py index 165a0da48..bfb8b5c18 100644 --- a/common/models/transactions.py +++ b/common/models/transactions.py @@ -53,6 +53,8 @@ class Transactions(HemeraModel): update_time = Column(TIMESTAMP, server_default=func.now()) reorg = Column(BOOLEAN, server_default=text("false")) + __query_order__ = [block_number, transaction_index] + @staticmethod def model_domain_mapping(): return [ diff --git a/common/services/postgresql_service.py b/common/services/postgresql_service.py index b1db34f31..5b1da7a86 100644 --- a/common/services/postgresql_service.py +++ b/common/services/postgresql_service.py @@ -21,29 +21,33 @@ def session_scope(session): class PostgreSQLService(object): - instance = None + _jdbc_instance = {} + _jdbc_initialized = set() - def __new__(cls, *args, **kwargs): - if cls.instance is None: - cls.instance = super().__new__(cls) - return cls.instance + def __new__(cls, jdbc_url, *args, **kwargs): + if jdbc_url not in cls._jdbc_instance: + instance = super().__new__(cls) + cls._jdbc_instance[jdbc_url] = instance + return cls._jdbc_instance[jdbc_url] def __init__(self, jdbc_url, db_version="head", script_location="migrations", init_schema=False): - self.db_version = db_version - self.engine = create_engine( - jdbc_url, - pool_size=10, - max_overflow=10, - pool_timeout=30, - pool_recycle=60, - connect_args={"application_name": "hemera_indexer"}, - ) - self.jdbc_url = jdbc_url - self.connection_pool = pool.SimpleConnectionPool(1, 10, jdbc_url) - - self.Session = sessionmaker(bind=self.engine) - if init_schema: - self.init_schema(script_location) + if jdbc_url not in self._jdbc_initialized: + self.db_version = db_version + self.engine = create_engine( + jdbc_url, + pool_size=10, + max_overflow=10, + pool_timeout=30, + pool_recycle=60, + connect_args={"application_name": "hemera_indexer"}, + ) + self.jdbc_url = jdbc_url + self.connection_pool = pool.SimpleConnectionPool(1, 10, jdbc_url) + + self.Session = sessionmaker(bind=self.engine) + if init_schema: + self.init_schema(script_location) + self._jdbc_initialized.add(jdbc_url) def get_conn(self): return self.connection_pool.getconn() diff --git a/common/utils/format_utils.py b/common/utils/format_utils.py index a3acace07..b280fede6 100644 --- a/common/utils/format_utils.py +++ b/common/utils/format_utils.py @@ -100,3 +100,7 @@ def format_coin_value_with_unit(value: int, native_token: str) -> str: return str(value) + " WEI" else: return "{0:.15f}".format(value / 10**18).rstrip("0").rstrip(".") + " " + native_token + + +def hex_to_bytes(hex_value: str) -> bytes: + return bytes.fromhex(hex_value[2:]) diff --git a/indexer/controller/scheduler/job_scheduler.py b/indexer/controller/scheduler/job_scheduler.py index d12b36861..d3762b5ef 100644 --- a/indexer/controller/scheduler/job_scheduler.py +++ b/indexer/controller/scheduler/job_scheduler.py @@ -14,6 +14,7 @@ from indexer.jobs.base_job import BaseExportJob, BaseJob, ExtensionJob, FilterTransactionDataJob from indexer.jobs.check_block_consensus_job import CheckBlockConsensusJob from indexer.jobs.export_blocks_job import ExportBlocksJob +from indexer.jobs.source_job.pg_source_job import PGSourceJob from indexer.utils.abi import bytes_to_hex_str from indexer.utils.exception_recorder import ExceptionRecorder @@ -42,6 +43,10 @@ def get_tokens_from_db(session): def get_source_job_type(source_path: str): if source_path.startswith("csvfile://"): return CSVSourceJob + elif source_path.startswith("postgresql://"): + return PGSourceJob + else: + raise ValueError(f"Unknown source job type with source path: {source_path}") class JobScheduler: @@ -55,6 +60,7 @@ def __init__( config={}, item_exporters=[ConsoleItemExporter()], required_output_types=[], + required_source_types=[], cache="memory", multicall=None, auto_reorg=True, @@ -71,6 +77,7 @@ def __init__( self.max_workers = max_workers self.config = config self.required_output_types = required_output_types + self.required_source_types = required_source_types self.load_from_source = config.get("source_path") if "source_path" in config else None self.jobs = [] self.job_classes = [] @@ -128,7 +135,21 @@ def get_data_buff(self): def discover_and_register_job_classes(self): if self.load_from_source: source_job = get_source_job_type(source_path=self.load_from_source) + if source_job is PGSourceJob: + source_job.output_types = self.required_source_types all_subclasses = [source_job] + + source_output_types = set(source_job.output_types) + for export_job in BaseExportJob.discover_jobs(): + skip = False + for output_type in export_job.output_types: + if output_type in source_output_types: + source_job.output_types = list(set(export_job.output_types + list(source_output_types))) + skip = True + break + if not skip: + all_subclasses.append(export_job) + else: all_subclasses = BaseExportJob.discover_jobs() @@ -146,7 +167,7 @@ def discover_and_register_job_classes(self): def instantiate_jobs(self): filters = [] for job_class in self.resolved_job_classes: - if job_class is ExportBlocksJob: + if job_class is ExportBlocksJob or job_class is PGSourceJob: continue job = job_class( required_output_types=self.required_output_types, @@ -179,6 +200,21 @@ def instantiate_jobs(self): filters=filters, ) self.jobs.insert(0, export_blocks_job) + else: + pg_source_job = PGSourceJob( + required_output_types=self.required_output_types, + batch_web3_provider=self.batch_web3_provider, + batch_web3_debug_provider=self.batch_web3_debug_provider, + item_exporters=self.item_exporters, + batch_size=self.batch_size, + multicall=self._is_multicall, + debug_batch_size=self.debug_batch_size, + max_workers=self.max_workers, + config=self.config, + is_filter=self.is_pipeline_filter, + filters=filters, + ) + self.jobs.insert(0, pg_source_job) if self.auto_reorg: check_job = CheckBlockConsensusJob( diff --git a/indexer/controller/stream_controller.py b/indexer/controller/stream_controller.py index 490a314e3..3c2d39bf1 100644 --- a/indexer/controller/stream_controller.py +++ b/indexer/controller/stream_controller.py @@ -2,12 +2,13 @@ import os import time -from common.utils.exception_control import HemeraBaseException +from common.utils.exception_control import FastShutdownError, HemeraBaseException from common.utils.file_utils import delete_file, write_to_file from common.utils.web3_utils import build_web3 from indexer.controller.base_controller import BaseController from indexer.controller.scheduler.job_scheduler import JobScheduler from indexer.utils.exception_recorder import ExceptionRecorder +from indexer.utils.limit_reader import LimitReader from indexer.utils.sync_recorder import BaseRecorder exception_recorder = ExceptionRecorder() @@ -20,6 +21,7 @@ def __init__( batch_web3_provider, sync_recorder: BaseRecorder, job_scheduler: JobScheduler, + limit_reader: LimitReader, max_retries=5, retry_from_record=False, delay=0, @@ -28,9 +30,10 @@ def __init__( self.sync_recorder = sync_recorder self.web3 = build_web3(batch_web3_provider) self.job_scheduler = job_scheduler + self.limit_reader = limit_reader self.max_retries = max_retries self.retry_from_record = retry_from_record - self.delay = 0 + self.delay = delay def action( self, @@ -72,7 +75,12 @@ def _do_stream(self, start_block, end_block, steps, retry_errors, period_seconds try: tries_reset = True - current_block = self._get_current_block_number() + current_block = self.limit_reader.get_current_block_number() + if current_block is None: + raise FastShutdownError( + "Can't get current limit block number from limit reader." + "If you're using PGLimitReader, please confirm blocks table has one record at least." + ) target_block = self._calculate_target_block(current_block, last_synced_block, end_block, steps) synced_blocks = max(target_block - last_synced_block, 0) diff --git a/indexer/domain/__init__.py b/indexer/domain/__init__.py index 7bfede621..6825db593 100644 --- a/indexer/domain/__init__.py +++ b/indexer/domain/__init__.py @@ -102,9 +102,11 @@ def dict_to_dataclass(data: Dict[str, Any], cls): if origin is list: # Handle lists item_type = args[0] - init_values[field] = [ - (dict_to_dataclass(item, item_type) if isinstance(item, dict) else item) for item in data[field] - ] + init_values[field] = ( + [(dict_to_dataclass(item, item_type) if isinstance(item, dict) else item) for item in data[field]] + if data[field] + else [] + ) elif hasattr(field_type, "__dataclass_fields__"): # Handle nested dataclass init_values[field] = dict_to_dataclass(data[field], field_type) diff --git a/indexer/domain/receipt.py b/indexer/domain/receipt.py index 9b948ae4c..6178f66a3 100644 --- a/indexer/domain/receipt.py +++ b/indexer/domain/receipt.py @@ -58,3 +58,23 @@ def from_rpc(receipt_dict: dict, block_timestamp=None, block_hash=None, block_nu to_int(hexstr=receipt_dict.get("blobGasPrice")) if receipt_dict.get("blobGasPrice") else None ), ) + + @staticmethod + def from_pg(table_row: dict): + return Receipt( + transaction_hash=table_row["hash"], + transaction_index=table_row["transaction_index"], + contract_address=table_row["receipt_contract_address"], + status=table_row["receipt_status"], + logs=[], + root=table_row["receipt_root"], + cumulative_gas_used=table_row["receipt_cumulative_gas_used"], + gas_used=table_row["receipt_gas_used"], + effective_gas_price=table_row["receipt_effective_gas_price"], + l1_fee=table_row["receipt_l1_fee"], + l1_fee_scalar=table_row["receipt_l1_fee_scalar"], + l1_gas_used=table_row["receipt_l1_gas_used"], + l1_gas_price=table_row["receipt_l1_gas_price"], + blob_gas_used=table_row["receipt_blob_gas_used"], + blob_gas_price=table_row["receipt_blob_gas_price"], + ) diff --git a/indexer/jobs/__init__.py b/indexer/jobs/__init__.py index 8376c8b7c..e47ba9893 100644 --- a/indexer/jobs/__init__.py +++ b/indexer/jobs/__init__.py @@ -1,5 +1,6 @@ __all__ = [ "CSVSourceJob", + "PGSourceJob", "ExportBlocksJob", "ExportTransactionsAndLogsJob", "ExportTokensAndTransfersJob", @@ -12,7 +13,6 @@ ] from indexer.jobs.base_job import FilterTransactionDataJob -from indexer.jobs.csv_source_job import CSVSourceJob from indexer.jobs.export_blocks_job import ExportBlocksJob from indexer.jobs.export_coin_balances_job import ExportCoinBalancesJob from indexer.jobs.export_contracts_job import ExportContractsJob @@ -21,3 +21,5 @@ from indexer.jobs.export_tokens_and_transfers_job import ExportTokensAndTransfersJob from indexer.jobs.export_traces_job import ExportTracesJob from indexer.jobs.export_transactions_and_logs_job import ExportTransactionsAndLogsJob +from indexer.jobs.source_job.csv_source_job import CSVSourceJob +from indexer.jobs.source_job.pg_source_job import PGSourceJob diff --git a/indexer/jobs/export_blocks_job.py b/indexer/jobs/export_blocks_job.py index e9f3e5c63..aa8f5cc21 100644 --- a/indexer/jobs/export_blocks_job.py +++ b/indexer/jobs/export_blocks_job.py @@ -18,21 +18,11 @@ ) from indexer.utils.json_rpc_requests import generate_get_block_by_number_json_rpc from indexer.utils.reorg import set_reorg_sign -from indexer.utils.utils import rpc_response_batch_to_results +from indexer.utils.utils import flatten, rpc_response_batch_to_results logger = logging.getLogger(__name__) -def flatten(lst): - result = [] - for item in lst: - if isinstance(item, list): - result.extend(flatten(item)) - else: - result.append(item) - return result - - # Exports blocks and block number <-> timestamp mapping class ExportBlocksJob(BaseExportJob): dependency_types = [] diff --git a/indexer/jobs/source_job/__init__.py b/indexer/jobs/source_job/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/indexer/jobs/csv_source_job.py b/indexer/jobs/source_job/csv_source_job.py similarity index 100% rename from indexer/jobs/csv_source_job.py rename to indexer/jobs/source_job/csv_source_job.py diff --git a/indexer/jobs/source_job/pg_source_job.py b/indexer/jobs/source_job/pg_source_job.py new file mode 100644 index 000000000..36dd7f3cc --- /dev/null +++ b/indexer/jobs/source_job/pg_source_job.py @@ -0,0 +1,500 @@ +import copy +import inspect +import logging +from collections import defaultdict +from datetime import datetime +from decimal import Decimal +from queue import Queue +from typing import List, Type, Union, get_args, get_origin + +from sqlalchemy import and_, func, select + +from common.converter.pg_converter import domain_model_mapping +from common.models.blocks import Blocks +from common.models.logs import Logs +from common.models.transactions import Transactions +from common.services.postgresql_service import PostgreSQLService +from common.utils.exception_control import FastShutdownError +from common.utils.format_utils import hex_to_bytes +from indexer.domain import Domain, dict_to_dataclass +from indexer.domain.block import Block +from indexer.domain.log import Log +from indexer.domain.receipt import Receipt +from indexer.domain.transaction import Transaction +from indexer.jobs.base_job import BaseSourceJob +from indexer.specification.specification import ( + AlwaysFalseSpecification, + AlwaysTrueSpecification, + FromAddressSpecification, + ToAddressSpecification, + TransactionFilterByLogs, + TransactionFilterByTransactionInfo, + TransactionHashSpecification, +) +from indexer.utils.utils import distinct_collections_by_group, flatten + +logger = logging.getLogger(__name__) + + +class PGSourceJob(BaseSourceJob): + output_types = [ + Block, + Transaction, + Log, + ] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._source_path = kwargs["config"].get("source_path", None) + self._service = PostgreSQLService(self._source_path) if self._source_path else None + self.pre_build = defaultdict(list) + self.post_build = defaultdict() + self.domain_mapping = defaultdict(dict) + self.pg_datas = defaultdict(list) + self._filters = flatten(kwargs.get("filters", [])) + self._is_filter = kwargs.get("is_filter", False) + self._specification = AlwaysFalseSpecification() if self._is_filter else AlwaysTrueSpecification() + + if self._service is None: + raise FastShutdownError("-pg or --postgres-url is required to run PGSourceJob") + + for output_type in self.output_types: + self._dataclass_build_dependence(output_type, Domain) + self.build_order = [] + self._calculate_build_queue() + + def _collect(self, **kwargs): + start_block = int(kwargs["start_block"]) + end_block = int(kwargs["end_block"]) + start_timestamp = self._query_timestamp_with_block(start_block) + end_timestamp = self._query_timestamp_with_block(end_block) + + self.pg_datas.clear() + if self._is_filter: + filter_blocks = set() + logs_transaction_hash = set() + transactions_hash = set() + for i, job_filter in enumerate(self._filters): + if isinstance(job_filter, TransactionFilterByLogs): + log_filter = defaultdict(list) + for filter_param in job_filter.get_eth_log_filters_params(): + param_address = filter_param["address"] + param_topics = flatten(filter_param["topics"]) + + if len(param_address) > 0: + log_filter["address"].extend(param_address) + + if len(param_topics) > 0: + log_filter["topics"].extend(param_topics) + + start_time = datetime.now() + logs = self._query_logs_filter(start_block, end_block, start_timestamp, end_timestamp, log_filter) + self.logger.info( + f"No.{i} filter: TransactionFilterByLogs finished. Took {datetime.now() - start_time}" + ) + self.pg_datas[Logs].extend(logs) + + for log in logs: + filter_blocks.add(log.block_number) + logs_transaction_hash.add("0x" + log.transaction_hash.hex()) + + elif isinstance(job_filter, TransactionFilterByTransactionInfo): + transaction_filter = defaultdict(list) + for spe in job_filter.specifications: + params = spe.to_filter_params() + if isinstance(spe, TransactionHashSpecification) and params: + transaction_filter["hash"].extend(params["hashes"]) + transactions_hash.add(params["hashes"]) + elif isinstance(spe, FromAddressSpecification): + transaction_filter["from_address"].append(params["from_address"]) + elif isinstance(spe, ToAddressSpecification): + transaction_filter["to_address"].append(params["to_address"]) + else: + raise ValueError(f"Unsupported transaction filter type: {type(filter)}") + + start_time = datetime.now() + transactions = self._query_transactions_filter( + start_block, end_block, start_timestamp, end_timestamp, transaction_filter + ) + self.logger.info( + f"No.{i} filter: TransactionFilterByTransactionInfo finished. " + f"Took {datetime.now() - start_time}" + ) + + self.pg_datas[Transactions].extend(transactions) + for transaction in transactions: + filter_blocks.add(transaction.block_number) + transactions_hash.add("0x" + transaction.hash.hex()) + else: + raise ValueError(f"Unsupported filter type: {type(filter)}") + + if len(logs_transaction_hash) > 0: + transaction_filter = { + "hash": list(logs_transaction_hash), + "from_address": [], + "to_address": [], + } + start_time = datetime.now() + transactions = self._query_transactions_filter( + start_block, end_block, start_timestamp, end_timestamp, transaction_filter + ) + self.logger.info( + f"Supplement transactions from filtered log list finished. Took {datetime.now() - start_time}" + ) + self.pg_datas[Transactions].extend(transactions) + + for transaction in transactions: + filter_blocks.add(transaction.block_number) + + if len(transactions_hash) > 0: + log_filter = { + "transaction_hash": list(transactions_hash), + "address": [], + "topics": [], + } + start_time = datetime.now() + logs = self._query_logs_filter(start_block, end_block, start_timestamp, end_timestamp, log_filter) + self.logger.info( + f"Supplement logs from filtered transaction list finished. Took {datetime.now() - start_time}" + ) + self.pg_datas[Logs].extend(logs) + + for log in logs: + filter_blocks.add(log.block_number) + + self.pg_datas[Logs] = distinct_collections_by_group( + collections=self.pg_datas[Logs], group_by=["transaction_hash", "log_index"] + ) + self.pg_datas[Transactions] = distinct_collections_by_group( + collections=self.pg_datas[Transactions], group_by=["hash"] + ) + + blocks = sorted(list(filter_blocks)) + else: + blocks = list(range(start_block, end_block + 1)) + + self._collect_from_pg(blocks, start_timestamp, end_timestamp) + + def _collect_from_pg(self, blocks, start_timestamp, end_timestamp): + + for output_type in self.output_types: + table = domain_model_mapping[output_type.__name__]["table"] + if len(self.pg_datas[table]) == 0: + start_time = datetime.now() + self.pg_datas[table] = self._query_with_blocks(table, blocks, start_timestamp, end_timestamp) + self.logger.info( + f"Read {table.__tablename__} from postgres finished. Took {datetime.now() - start_time}" + ) + + def _process(self, **kwargs): + self.domain_mapping.clear() + for output_type in self.build_order: + table = domain_model_mapping[output_type.__name__]["table"] + domains = self._dataclass_build(self.pg_datas[table], output_type) + if hasattr(table, "__query_order__"): + domains.sort(key=lambda x: tuple(getattr(x, column.name) for column in table.__query_order__)) + self._data_buff[output_type.type()] = domains + + def _export(self): + pass + + def _query_timestamp_with_block(self, block_number): + session = self._service.get_service_session() + try: + timestamp = session.query(Blocks.timestamp).filter(Blocks.number == block_number).scalar() + finally: + session.close() + + return timestamp + + def _query_with_blocks(self, table, blocks, start_timestamp, end_timestamp): + if len(blocks) == 0: + return [] + + session = self._service.get_service_session() + unnest_query = select(func.unnest(blocks).label("block_number")).subquery() + + try: + if hasattr(table, "number") and hasattr(table, "timestamp"): + sub_table = ( + select(table) + .filter(and_(table.timestamp >= start_timestamp, table.timestamp <= end_timestamp)) + .subquery(table.__tablename__) + ) + + result = ( + session.query(sub_table) + .join(unnest_query, sub_table.c.number == unnest_query.c.block_number) + .order_by(*table.__query_order__) + .all() + ) + elif hasattr(table, "block_number") and hasattr(table, "block_timestamp"): + sub_table = ( + select(table) + .filter(and_(table.block_timestamp >= start_timestamp, table.block_timestamp <= end_timestamp)) + .subquery(table.__tablename__) + ) + + result = ( + session.query(sub_table).join(unnest_query, sub_table.c.block_number == unnest_query.c.block_number) + # .order_by(*table.__query_order__) + .all() + ) + else: + result = [] + + finally: + session.close() + + return result + + def _query_logs_filter(self, start_block, end_block, start_timestamp, end_timestamp, log_filter): + logs = [] + conditions = True + session = self._service.get_service_session() + + try: + + if len(log_filter["address"]) > 0 and len(log_filter["topics"]) > 0: + conditions = and_( + Logs.address.in_([hex_to_bytes(address) for address in set(log_filter["address"])]), + Logs.topic0.in_([hex_to_bytes(topic0) for topic0 in set(log_filter["topics"])]), + ) + elif len(log_filter["address"]) > 0: + conditions = Logs.address.in_([hex_to_bytes(address) for address in set(log_filter["address"])]) + elif len(log_filter["topics"]) > 0: + conditions = Logs.topic0.in_([hex_to_bytes(topic0) for topic0 in set(log_filter["topics"])]) + + if len(log_filter["address"]) > 0 or len(log_filter["topics"]) > 0: + query_filter = and_( + Logs.block_timestamp >= start_timestamp, + Logs.block_timestamp <= end_timestamp, + conditions, + Logs.block_number >= start_block, + Logs.block_number <= end_block, + ) + logs.extend(session.query(Logs).filter(query_filter).all()) + + if len(log_filter["transaction_hash"]) > 0: + conditions = Logs.transaction_hash.in_( + [hex_to_bytes(transaction_hash) for transaction_hash in set(log_filter["transaction_hash"])] + ) + + query_filter = and_( + Logs.block_timestamp >= start_timestamp, + Logs.block_timestamp <= end_timestamp, + conditions, + Logs.block_number >= start_block, + Logs.block_number <= end_block, + ) + logs.extend(session.query(Logs).filter(query_filter).all()) + finally: + session.close() + + return logs + + def _query_transactions_filter(self, start_block, end_block, start_timestamp, end_timestamp, transaction_filter): + transactions = [] + session = self._service.get_service_session() + + try: + + if len(transaction_filter["hash"]) > 0: + conditions = Transactions.hash.in_( + [hex_to_bytes(transaction_hash) for transaction_hash in set(transaction_filter["hash"])] + ) + query_filter = and_( + Transactions.block_timestamp >= start_timestamp, + Transactions.block_timestamp <= end_timestamp, + conditions, + Transactions.block_number >= start_block, + Transactions.block_number <= end_block, + ) + transactions.extend(session.query(Transactions).filter(query_filter).all()) + + if len(transaction_filter["from_address"]) > 0: + conditions = Transactions.from_address.in_( + [hex_to_bytes(from_address) for from_address in set(transaction_filter["from_address"])] + ) + query_filter = and_( + Transactions.block_timestamp >= start_timestamp, + Transactions.block_timestamp <= end_timestamp, + conditions, + Transactions.block_number >= start_block, + Transactions.block_number <= end_block, + ) + transactions.extend(session.query(Transactions).filter(query_filter).all()) + + if len(transaction_filter["to_address"]) > 0: + conditions = Transactions.to_address.in_( + [hex_to_bytes(to_address) for to_address in set(transaction_filter["to_address"])] + ) + query_filter = and_( + Transactions.block_timestamp >= start_timestamp, + Transactions.block_timestamp <= end_timestamp, + conditions, + Transactions.block_number >= start_block, + Transactions.block_number <= end_block, + ) + transactions.extend(session.query(Transactions).filter(query_filter).all()) + + if ( + len(transaction_filter["hash"]) == 0 + and len(transaction_filter["from_address"]) == 0 + and len(transaction_filter["to_address"]) == 0 + ): + query_filter = and_( + Transactions.block_timestamp >= start_timestamp, + Transactions.block_timestamp <= end_timestamp, + Transactions.block_number >= start_block, + Transactions.block_number <= end_block, + ) + transactions.extend(session.query(Transactions).filter(query_filter).all()) + + finally: + session.close() + + return transactions + + def _dataclass_build(self, pg_datas, output_type): + + def build_block(): + blocks = [table_to_dataclass(data, Block) for data in pg_datas] + self.domain_mapping[output_type] = {block.hash: block for block in blocks} + for block in blocks: + block.transactions = [] + + return blocks + + def build_transaction(): + transactions = [table_to_dataclass(data, Transaction) for data in pg_datas] + self.domain_mapping[output_type] = {transaction.hash: transaction for transaction in transactions} + if Block in self.output_types: + for transaction in transactions: + self.domain_mapping[Block][transaction.block_hash].transactions.append(transaction) + + return transactions + + def build_log(): + logs = [table_to_dataclass(data, Log) for data in pg_datas] + if Transaction in self.output_types: + for log in logs: + self.domain_mapping[Transaction][log.transaction_hash].receipt.logs.append(log) + + return logs + + special_build = { + Block: build_block, + Transaction: build_transaction, + Log: build_log, + } + + if output_type in special_build: + domains = special_build[output_type]() + else: + domains = [table_to_dataclass(data, output_type) for data in pg_datas] + + return domains + + def _dataclass_build_dependence(self, cls_type: Type[Domain], target_type): + field_types = {f.name: f.type for f in cls_type.__dataclass_fields__.values()} + + for field, field_type in field_types.items(): + is_dependent, dependent_type = check_dependency(field_type, Domain) + if is_dependent: + self.pre_build[cls_type].append(dependent_type) + self.post_build[dependent_type] = cls_type + if dependent_type not in self.output_types: + self._dataclass_build_dependence(dependent_type, target_type) + + def _calculate_build_queue(self): + build_queue = Queue() + un_build_outputs = copy.copy(self.output_types) + while len(un_build_outputs) > 0: + for output_type in un_build_outputs: + if output_type not in self.post_build: + build_queue.put(output_type) + un_build_outputs.remove(output_type) + + if output_type in self.pre_build: + for cls_type in self.pre_build[output_type]: + if cls_type in self.post_build: + del self.post_build[cls_type] + if cls_type is Receipt: + for post_type in self.pre_build[cls_type]: + del self.post_build[post_type] + + while not build_queue.empty(): + self.build_order.append(build_queue.get()) + + +def check_dependency(column_type, target_type) -> (bool, object): + is_dependent = False + if get_origin(column_type) is Union: + for arg in get_args(column_type): + is_dependent, dependent_type = check_dependency(arg, target_type) + if is_dependent: + return is_dependent, dependent_type + return is_dependent, None + + if get_origin(column_type) is list or column_type is List: + if get_args(column_type): + return check_dependency(get_args(column_type)[0], target_type) + return False, None + + if column_type is target_type: + return True, target_type + + if inspect.isclass(column_type) and issubclass(column_type, target_type): + return True, column_type + + return False, None + + +def table_to_dataclass(row_instance, cls): + """ + Converts row of table to a dataclass instance, handling nested structures. + + Args: + row_instance (HemeraModel): The input data structure. + cls: The dataclass type to convert to. + + Returns: + An instance of the dataclass which is corresponding to table in the definition. + """ + + dict_instance = {} + if hasattr(row_instance, "__table__"): + for column in row_instance.__table__.columns: + if column.name == "meta_data": + meta_data_json = getattr(row_instance, column.name) + if meta_data_json: + for key in meta_data_json: + dict_instance[key] = meta_data_json[key] + else: + value = getattr(row_instance, column.name) + dict_instance[column.name] = convert_value(value) + else: + for column, value in row_instance._asdict().items(): + dict_instance[column] = convert_value(value) + + domain = dict_to_dataclass(dict_instance, cls) + if cls is Transaction: + domain.fill_with_receipt(Receipt.from_pg(dict_instance)) + + return domain + + +def convert_value(value): + if isinstance(value, datetime): + return int(round(value.timestamp())) + elif isinstance(value, Decimal): + return float(value) + elif isinstance(value, bytes): + return "0x" + value.hex() + elif isinstance(value, list): + return [convert_value(v) for v in value] + elif isinstance(value, dict): + return {k: convert_value(v) for k, v in value.items()} + else: + return value diff --git a/indexer/specification/specification.py b/indexer/specification/specification.py index 3c517e9da..a1c988274 100644 --- a/indexer/specification/specification.py +++ b/indexer/specification/specification.py @@ -61,6 +61,12 @@ def __init__(self, address): def is_satisfied_by(self, item: Transaction): return item.from_address == self.address + def to_filter_params(self): + params = None + if self.address: + params = {"from_address": self.address} + return params + class ToAddressSpecification(Specification): def __init__(self, address): @@ -69,6 +75,12 @@ def __init__(self, address): def is_satisfied_by(self, item: Transaction): return item.to_address == self.address + def to_filter_params(self): + params = None + if self.address: + params = {"to_address": self.address} + return params + class FuncSignSpecification(Specification): def __init__(self, func_sign): @@ -110,6 +122,12 @@ def __init__(self, hashes: List[str]): def is_satisfied_by(self, item: Transaction): return item.hash in self.hashes + def to_filter_params(self): + params = None + if self.hashes: + params = {"hashes": self.hashes} + return params + class TransactionFilterByLogs: def __init__(self, topics_filters: List[TopicSpecification]): diff --git a/indexer/utils/limit_reader.py b/indexer/utils/limit_reader.py new file mode 100644 index 000000000..bb99fc219 --- /dev/null +++ b/indexer/utils/limit_reader.py @@ -0,0 +1,49 @@ +from sqlalchemy import func + +from common.models.blocks import Blocks +from common.services.postgresql_service import PostgreSQLService +from common.utils.exception_control import FastShutdownError +from common.utils.web3_utils import build_web3 +from indexer.utils.thread_local_proxy import ThreadLocalProxy + + +class LimitReader(object): + def get_current_block_number(self): + pass + + +class RPCLimitReader(LimitReader): + + def __init__(self, **kwargs): + self.rpc_uri = kwargs.get("rpc_uri") + self.web3 = build_web3(self.rpc_uri) + + def get_current_block_number(self): + return int(self.web3.eth.block_number) + + +class PGLimitReader(LimitReader): + + def __init__(self, **kwargs): + self.postgres_uri = kwargs.get("postgres_uri") + self.service = PostgreSQLService(jdbc_url=self.postgres_uri) + + def get_current_block_number(self): + session = self.service.get_service_session() + try: + block_number = session.query(func.max(Blocks.number)).scalar() + finally: + session.close() + + return block_number + + +def create_limit_reader(postgres_uri: str, rpc_uri: ThreadLocalProxy) -> LimitReader: + if postgres_uri and postgres_uri.startswith("postgresql://"): + return PGLimitReader(postgres_uri=postgres_uri) + elif rpc_uri is not None: + return RPCLimitReader(rpc_uri=rpc_uri) + else: + raise FastShutdownError( + f"Unable to create limit reader with parameter postgres_uri:{postgres_uri} and rpc_uri:{rpc_uri}" + ) diff --git a/indexer/utils/parameter_utils.py b/indexer/utils/parameter_utils.py index d64ca0cf4..fce090b82 100644 --- a/indexer/utils/parameter_utils.py +++ b/indexer/utils/parameter_utils.py @@ -4,6 +4,8 @@ import click +from common.utils.format_utils import to_snake_case +from indexer.domain import Domain from indexer.exporters.item_exporter import ItemExporterType, check_exporter_in_chosen @@ -27,7 +29,22 @@ def check_file_exporter_parameter(outputs, block_batch_size, blocks_per_file): ) -def check_file_load_parameter(cli_path: str): +def check_source_load_parameter(cli_path: str, start_block=None, end_block=None, auto_reorg=False): + if auto_reorg: + raise click.ClickException( + "Combine with read from source, --auto-reorg should keep its default value of false." + "If you worried about data correctness with reorg, you could set --delay for indexing confirmed blocks" + ) + + if cli_path.startswith("postgresql://"): + return + + if start_block is not None or end_block is not None: + raise click.ClickException( + "--source-path specify a file source. " + "Combine with file source, --start-block and --end-block should be given." + ) + load_file_path = extract_path_from_parameter(cli_path) if not os.path.exists(load_file_path): @@ -42,3 +59,20 @@ def check_file_load_parameter(cli_path: str): "Providing data path does not have any .csv or .json file. " "The Following custom job will not have any data input. " ) + + +def generate_dataclass_type_list_from_parameter(require_types: str, generate_type: str): + domain_dict = Domain.get_all_domain_dict() + parse_output_types = set() + + for output_type in require_types.split(","): + output_type = to_snake_case(output_type) + if output_type not in domain_dict: + raise click.ClickException(f"{generate_type} type {output_type} is not supported") + parse_output_types.add(domain_dict[output_type]) + + if not require_types: + raise click.ClickException(f"No {generate_type} types provided") + types = list(set(parse_output_types)) + + return types diff --git a/indexer/utils/utils.py b/indexer/utils/utils.py index 078641368..b9f791eb1 100644 --- a/indexer/utils/utils.py +++ b/indexer/utils/utils.py @@ -146,7 +146,7 @@ def merge_sort(sorted_col_a, sorted_col_b): return merged -def distinct_collections_by_group(collections: List[Domain], group_by: List[str], max_key: Union[str, None] = None): +def distinct_collections_by_group(collections: List[object], group_by: List[str], max_key: Union[str, None] = None): distinct = {} for item in collections: key = tuple(getattr(item, idx) for idx in group_by) @@ -162,3 +162,13 @@ def distinct_collections_by_group(collections: List[Domain], group_by: List[str] def format_block_id(block_id: Union[Optional[int], str]) -> str: return hex(block_id) if block_id and isinstance(block_id, int) else block_id + + +def flatten(lst): + result = [] + for item in lst: + if isinstance(item, list): + result.extend(flatten(item)) + else: + result.append(item) + return result From 15b3019fb20cd920ddef363a4bd65a2d0f0cb1e0 Mon Sep 17 00:00:00 2001 From: li xiang Date: Sat, 12 Oct 2024 18:09:35 +0800 Subject: [PATCH 08/12] Add v2 aci features (#179) * Add v2 aci features * Add time metrics * Configure features dynamically using decorators --- api/app/address/__init__.py | 2 + api/app/address/features.py | 28 ++ api/app/address/routes.py | 116 ++++- api/app/af_ens/routes.py | 291 ------------ api/app/api.py | 4 +- api/app/main.py | 12 +- .../deposit_to_l2/endpoint}/__init__.py | 0 .../custom/deposit_to_l2/endpoint}/routes.py | 205 +++++---- .../custom/hemera_ens/endpoint/__init__.py | 6 + .../hemera_ens/endpoint}/action_types.py | 0 .../custom/hemera_ens/endpoint/routes.py | 309 +++++++++++++ .../modules/custom/opensea/endpoint/routes.py | 46 +- indexer/modules/custom/opensea/opensea_job.py | 10 +- .../custom/uniswap_v3/endpoints/routes.py | 425 ++++++++++++++---- 14 files changed, 944 insertions(+), 510 deletions(-) create mode 100644 api/app/address/features.py delete mode 100644 api/app/af_ens/routes.py rename {api/app/deposit_to_l2 => indexer/modules/custom/deposit_to_l2/endpoint}/__init__.py (100%) rename {api/app/deposit_to_l2 => indexer/modules/custom/deposit_to_l2/endpoint}/routes.py (52%) create mode 100644 indexer/modules/custom/hemera_ens/endpoint/__init__.py rename {api/app/af_ens => indexer/modules/custom/hemera_ens/endpoint}/action_types.py (100%) create mode 100644 indexer/modules/custom/hemera_ens/endpoint/routes.py diff --git a/api/app/address/__init__.py b/api/app/address/__init__.py index c72b9a720..45da427bd 100644 --- a/api/app/address/__init__.py +++ b/api/app/address/__init__.py @@ -1,3 +1,5 @@ +from functools import wraps + from flask_restx.namespace import Namespace address_features_namespace = Namespace( diff --git a/api/app/address/features.py b/api/app/address/features.py new file mode 100644 index 000000000..dc67d2f44 --- /dev/null +++ b/api/app/address/features.py @@ -0,0 +1,28 @@ +from functools import wraps + +feature_router = {} + + +class FeatureRegistry: + def __init__(self): + self.features = {} + self.feature_list = [] + + def register(self, feature_name, subcategory): + def decorator(f): + if feature_name not in self.features: + self.features[feature_name] = {} + self.feature_list.append(feature_name) + self.features[feature_name][subcategory] = f + + @wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + + return wrapper + + return decorator + + +feature_registry = FeatureRegistry() +register_feature = feature_registry.register diff --git a/api/app/address/routes.py b/api/app/address/routes.py index 1a4cd6a07..23cc515f0 100644 --- a/api/app/address/routes.py +++ b/api/app/address/routes.py @@ -1,15 +1,17 @@ -from time import time -from typing import Union +import logging +import time +from typing import Any, Dict, Optional, Union import flask +from flask import request from flask_restx import Resource from sqlalchemy import func from api.app.address import address_features_namespace +from api.app.address.features import feature_registry, register_feature from api.app.address.models import AddressBaseProfile, ScheduledMetadata -from api.app.af_ens.routes import ACIEnsCurrent, ACIEnsDetail from api.app.cache import cache -from api.app.deposit_to_l2.routes import ACIDepositToL2Current, ACIDepositToL2Transactions +from api.app.main import app from common.models import db from common.utils.exception_control import APIError from common.utils.format_utils import as_dict, format_to_dict @@ -18,9 +20,10 @@ get_address_deploy_contract_count, get_address_first_deploy_contract_time, ) +from indexer.modules.custom.deposit_to_l2.endpoint.routes import ACIDepositToL2Current, ACIDepositToL2Transactions +from indexer.modules.custom.hemera_ens.endpoint.routes import ACIEnsCurrent, ACIEnsDetail from indexer.modules.custom.opensea.endpoint.routes import ACIOpenseaProfile, ACIOpenseaTransactions from indexer.modules.custom.uniswap_v3.endpoints.routes import ( - UniswapV3WalletLiquidityDetail, UniswapV3WalletLiquidityHolding, UniswapV3WalletTradingRecords, UniswapV3WalletTradingSummary, @@ -32,6 +35,8 @@ MAX_INTERNAL_TRANSACTION = 10000 MAX_TOKEN_TRANSFER = 10000 +logger = app.logger + def get_address_recent_info(address: bytes, last_timestamp: int) -> dict: pass @@ -84,14 +89,37 @@ def get(self, address): return profile, 200 +@register_feature("contract_deployer", "value") +def get_contract_deployer_profile(address) -> Optional[Dict[str, Any]]: + address_deploy_contract_count = get_address_deploy_contract_count(address) + address_first_deploy_contract_time = get_address_first_deploy_contract_time(address) + return ( + { + "deployed_countract_count": address_deploy_contract_count, + "first_deployed_time": address_first_deploy_contract_time, + } + if address_deploy_contract_count != 0 + else None + ) + + +@register_feature("contract_deployer", "events") +def get_contract_deployed_events(address, limit=5, offset=0) -> Optional[Dict[str, Any]]: + count = get_address_deploy_contract_count(address) + if count == 0: + return None + events = get_address_contract_operations(address, limit=limit, offset=offset) + res = [] + for event in events: + res.append(format_to_dict(event)) + return {"data": res, "total": count} + + @address_features_namespace.route("/v1/aci/
/contract_deployer/profile") class ACIContractDeployerProfile(Resource): def get(self, address): address = address.lower() - return { - "deployed_countract_count": get_address_deploy_contract_count(address), - "first_deployed_time": get_address_first_deploy_contract_time(address), - } + return get_contract_deployer_profile(address) or {"deployed_countract_count": 0, "first_deployed_time": None} @address_features_namespace.route("/v1/aci/
/contract_deployer/events") @@ -102,11 +130,11 @@ def get(self, address): page_size = int(flask.request.args.get("size", PAGE_SIZE)) limit = page_size offset = (page_index - 1) * page_size - events = get_address_contract_operations(address, limit=limit, offset=offset) - res = [] - for event in events: - res.append(format_to_dict(event)) - return {"data": res} + + return (get_contract_deployed_events(address, limit=limit, offset=offset) or {"data": [], "total": 0}) | { + "size": page_size, + "page": page_index, + } @address_features_namespace.route("/v1/aci/
/all_features") @@ -126,7 +154,7 @@ def get(self, address): if features: feature_list = features.split(",") - timer = time() + timer = time.time() feature_result = {} if "contract_deployer" in feature_list: @@ -178,7 +206,7 @@ def get(self, address): } ) - print(time() - timer) + print(time.time() - timer) combined_result = { "address": address, @@ -186,3 +214,59 @@ def get(self, address): } return combined_result, 200 + + +@address_features_namespace.route("/v2/aci/
/all_features") +class ACIAllFeatures(Resource): + def get(self, address): + address = address.lower() + requested_features = request.args.get("features") + + if requested_features: + feature_list = [f for f in requested_features.split(",") if f in feature_registry.feature_list] + else: + feature_list = feature_registry.feature_list + + feature_result = {} + total_start_time = time.time() + + for feature in feature_list: + feature_start_time = time.time() + feature_result[feature] = {} + for subcategory in feature_registry.features[feature]: + subcategory_start_time = time.time() + try: + feature_result[feature][subcategory] = feature_registry.features[feature][subcategory](address) + subcategory_end_time = time.time() + logger.debug( + f"Feature '{feature}' subcategory '{subcategory}' execution time: {subcategory_end_time - subcategory_start_time:.4f} seconds" + ) + except Exception as e: + logger.error(f"Error in feature '{feature}' subcategory '{subcategory}': {str(e)}") + feature_result[feature][subcategory] = {"error": str(e)} + + feature_end_time = time.time() + logger.debug( + f"Total execution time for feature '{feature}': {feature_end_time - feature_start_time:.4f} seconds" + ) + + feature_data_list = [ + {"id": feature_id, **subcategory_dict} + for feature_id in feature_list + if ( + subcategory_dict := { + subcategory: feature_result[feature_id][subcategory] + for subcategory in feature_registry.features[feature_id] + if feature_result[feature_id][subcategory] is not None + } + ) + ] + combined_result = { + "address": address, + "features": feature_data_list, + } + + total_end_time = time.time() + logger.debug(f"Total execution time for all features: {total_end_time - total_start_time:.4f} seconds") + + return combined_result, 200 diff --git a/api/app/af_ens/routes.py b/api/app/af_ens/routes.py deleted file mode 100644 index cd09e47ba..000000000 --- a/api/app/af_ens/routes.py +++ /dev/null @@ -1,291 +0,0 @@ -import decimal -from datetime import date, datetime -from time import time - -from flask_restx import Resource -from flask_restx.namespace import Namespace -from sqlalchemy.sql import and_, or_ -from web3 import Web3 - -from api.app.af_ens.action_types import OperationType -from common.models import db -from common.models.current_token_balances import CurrentTokenBalances -from common.models.erc721_token_id_details import ERC721TokenIdDetails -from common.models.erc721_token_transfers import ERC721TokenTransfers -from common.models.erc1155_token_transfers import ERC1155TokenTransfers -from common.utils.config import get_config -from common.utils.exception_control import APIError -from indexer.modules.custom.hemera_ens.models.af_ens_address_current import ENSAddress -from indexer.modules.custom.hemera_ens.models.af_ens_event import ENSMiddle -from indexer.modules.custom.hemera_ens.models.af_ens_node_current import ENSRecord - -app_config = get_config() - -w3 = Web3(Web3.HTTPProvider("https://ethereum-rpc.publicnode.com")) -af_ens_namespace = Namespace("User Operation Namespace", path="/", description="ENS feature") - - -PAGE_SIZE = 10 - - -@af_ens_namespace.route("/v1/aci/
/ens/current") -class ACIEnsCurrent(Resource): - def get(self, address): - res = { - "primary_name": None, - "resolve_to_names": [], - "ens_holdings": [], - "ens_holdings_total": None, - "first_register_time": None, - "first_set_primary_time": None, - } - try: - address = bytes.fromhex(address[2:]) - except Exception: - raise APIError("Invalid Address Format", code=400) - - dn = datetime.now() - # current_address holds 721 & 1155 tokens - ens_token_address = bytes.fromhex("57F1887A8BF19B14FC0DF6FD9B2ACC9AF147EA85") - all_721_owns = ( - db.session.query(ERC721TokenIdDetails) - .filter( - and_( - ERC721TokenIdDetails.token_owner == address, ERC721TokenIdDetails.token_address == ens_token_address - ) - ) - .all() - ) - all_721_ids = [r.token_id for r in all_721_owns] - all_owned = ( - db.session.query(ENSRecord) - .filter(and_(ENSRecord.token_id.in_(all_721_ids), ENSRecord.w_token_id.is_(None))) - .all() - ) - all_owned_map = {r.token_id: r for r in all_owned} - for id in all_721_ids: - r = all_owned_map.get(id) - res["ens_holdings"].append( - { - "name": r.name if r else None, - "is_expire": r.expires < dn if r and r.expires else True, - "type": "ERC721", - "token_id": str(id), - } - ) - ens_1155_address = bytes.fromhex("d4416b13d2b3a9abae7acd5d6c2bbdbe25686401") - all_1155_owns = ( - db.session.query(CurrentTokenBalances) - .filter( - and_(CurrentTokenBalances.address == address, CurrentTokenBalances.token_address == ens_1155_address) - ) - .all() - ) - all_1155_ids = [r.token_id for r in all_1155_owns] - all_owned_1155_ens = ( - db.session.query(ENSRecord) - .filter(and_(ENSRecord.expires >= dn, ENSRecord.w_token_id.in_(all_1155_ids))) - .all() - ) - res["ens_holdings"].extend( - [ - {"name": r.name, "is_expire": r.expires < dn, "type": "ERC1155", "token_id": str(r.w_token_id)} - for r in all_owned_1155_ens - if r.name and r.name.endswith(".eth") - ] - ) - res["ens_holdings_total"] = len(res["ens_holdings"]) - primary_address_row = db.session.query(ENSAddress).filter(ENSAddress.address == address).first() - if primary_address_row: - res["primary_name"] = primary_address_row.name - else: - res["primary_name"] = w3.ens.name(w3.to_checksum_address(w3.to_hex(address))) - - be_resolved_ens = db.session.query(ENSRecord).filter(and_(ENSRecord.address == address)).all() - res["resolve_to_names"] = [ - { - "name": r.name, - "is_expire": r.expires < dn if r and r.expires else True, - } - for r in be_resolved_ens - if r.name and r.name.endswith(".eth") - ] - res["resolve_to_names_total"] = len(res["resolve_to_names"]) - - first_register = ( - db.session.query(ENSMiddle) - .filter( - and_( - ENSMiddle.from_address == address, - or_(ENSMiddle.event_name == "NameRegistered", ENSMiddle.event_name == "HashRegistered"), - ) - ) - .order_by(ENSMiddle.block_number) - .first() - ) - if first_register: - res["first_register_time"] = datetime_to_string(first_register.block_timestamp) - first_set_name = ( - db.session.query(ENSMiddle) - .filter( - and_( - ENSMiddle.from_address == address, - or_(ENSMiddle.method == "setName", ENSMiddle.event_name == "NameChanged"), - ) - ) - .order_by(ENSMiddle.block_number) - .first() - ) - if first_set_name: - res["first_set_primary_time"] = datetime_to_string(first_set_name.block_timestamp) - - return res - - -@af_ens_namespace.route("/v1/aci/
/ens/detail") -class ACIEnsDetail(Resource): - def get(self, address): - try: - address = bytes.fromhex(address[2:]) - except Exception: - raise APIError("Invalid Address Format", code=400) - - events = [] - all_records_rows = ( - db.session.query(ENSMiddle) - .filter(ENSMiddle.from_address == address) - .order_by(ENSMiddle.block_number.desc(), ENSMiddle.log_index.desc()) - .limit(PAGE_SIZE) - .all() - ) - # erc721_ids = list({r.token_id for r in all_records_rows if r.token_id}) - # erc721_id_transfers = ( - # db.session.query(ERC721TokenTransfers) - # .filter(ERC721TokenTransfers.token_id.in_(erc721_ids)) - # .order_by(ERC721TokenTransfers.block_number) - # .all() - # ) - # - # erc1155_ids = list({r.w_token_id for r in all_records_rows if r.w_token_id}) - # erc1155_id_transfers = ( - # db.session.query(ERC1155TokenTransfers) - # .filter(ERC1155TokenTransfers.token_id.in_(erc1155_ids)) - # .order_by(ERC1155TokenTransfers.block_number) - # .all() - # ) - - node_name_map = {} - for r in all_records_rows: - if r.name: - if r.name.endswith(".eth"): - node_name_map[r.node] = r.name - else: - node_name_map[r.node] = r.name + ".eth" - token_id_name_map = dict() - for r in all_records_rows: - if r.token_id: - token_id_name_map[r.token_id] = node_name_map[r.node] - if r.w_token_id: - token_id_name_map[r.w_token_id] = node_name_map[r.node] - - all_rows = merge_ens_middle(all_records_rows) - - for r in all_rows: - name = None - if hasattr(r, "node") and r.node and r.node in node_name_map: - name = node_name_map[r.node] - elif hasattr(r, "token_id") and r.token_id and r.token_id in token_id_name_map: - name = token_id_name_map[r.token_id] - elif hasattr(r, "w_token_id") and r.w_token_id and r.w_token_id in token_id_name_map: - name = token_id_name_map[r.w_token_id] - base = { - "block_number": r.block_number, - "block_timestamp": datetime_to_string(r.block_timestamp), - "transaction_hash": "0x" + r.transaction_hash.hex(), - "name": name, - } - - extras = get_action_type(r) - base.update(extras) - events.append(base) - - return { - "data": events, - "total": len(events), - "page": 1, - "size": PAGE_SIZE, - } - - -def merge_ens_middle(records): - """Merge consecutive records when setAddr and nameRegistered are duplicated""" - if not records: - return [] - - res = [records[0]] - - for current_record in records[1:]: - previous_record = res[-1] - - # Check if the current record should be merged with the previous one - if ( - current_record.event_name == previous_record.event_name == "AddressChanged" - and current_record.name == previous_record.name - ) or ( - current_record.event_name == previous_record.event_name == "NameRegistered" - and current_record.name == previous_record.name - ): - for column in ENSMiddle.__table__.columns: - current_value = getattr(current_record, column.name) - previous_value = getattr(previous_record, column.name) - if previous_value is None and current_value is not None: - setattr(previous_record, column.name, current_value) - else: - res.append(current_record) - - return res - - -def get_action_type(record): - if isinstance(record, ERC721TokenTransfers) or isinstance(record, ERC1155TokenTransfers): - return { - "action_type": OperationType.TRANSFER.value, - "from": "0x" + record.from_address.hex(), - "to": "0x" + record.to_address.hex(), - "token_id": int(record.token_id), - } - if record.method == "setName" or record.event_name == "NameChanged": - return {"action_type": OperationType.SET_PRIMARY_NAME.value} - if record.event_name == "NameRegistered" or record.event_name == "HashRegistered": - return {"action_type": OperationType.REGISTER.value} - if record.event_name == "NameRenewed": - return {"action_type": OperationType.RENEW.value, "expires": datetime_to_string(record.expires)} - if record.event_name == "AddressChanged": - return {"action_type": OperationType.SET_RESOLVED_ADDRESS.value, "address": "0x" + record.address.hex()} - raise ValueError("Unknown operation type") - - -def datetime_to_string(dt, format="%Y-%m-%d %H:%M:%S"): - if isinstance(dt, datetime): - return dt.astimezone().isoformat("T", "seconds") - elif isinstance(dt, date): - return dt.astimezone().isoformat("T", "seconds") - elif dt is None: - return None - else: - raise ValueError(f"Unsupported type for dt: {type(dt)}. Expected datetime or date.") - - -def model_to_dict(instance): - res = {} - for c in instance.__table__.columns: - v = getattr(instance, c.name) - if isinstance(v, datetime): - res[c.name] = v.isoformat() - elif isinstance(v, bytes): - res[c.name] = "0x" + v.hex() - elif isinstance(v, decimal.Decimal): - res[c.name] = str(v) - else: - res[c.name] = v - return res diff --git a/api/app/api.py b/api/app/api.py index a2321fbe9..5adc445fb 100644 --- a/api/app/api.py +++ b/api/app/api.py @@ -4,11 +4,11 @@ from flask_restx import Api from api.app.address.routes import address_features_namespace -from api.app.af_ens.routes import af_ens_namespace from api.app.contract.routes import contract_namespace -from api.app.deposit_to_l2.routes import token_deposit_namespace from api.app.explorer.routes import explorer_namespace from api.app.user_operation.routes import user_operation_namespace +from indexer.modules.custom.deposit_to_l2.endpoint.routes import token_deposit_namespace +from indexer.modules.custom.hemera_ens.endpoint import af_ens_namespace from indexer.modules.custom.merchant_moe.endpoints.routes import merchant_moe_namespace from indexer.modules.custom.opensea.endpoint.routes import opensea_namespace from indexer.modules.custom.staking_fbtc.endpoints.routes import staking_namespace diff --git a/api/app/main.py b/api/app/main.py index e1ccbbc89..869e7fd3f 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -18,11 +18,21 @@ config = get_config() -logging.basicConfig(level=logging.INFO) +import logging +import os + # logging.getLogger("sqlalchemy.pool").setLevel(logging.DEBUG) app = Flask(__name__) +# Get the log level from the environment variable, default to WARNING if not set +log_level = os.environ.get("LOG_LEVEL", "INFO").upper() + +# Convert the string log level to the corresponding numeric value +numeric_level = getattr(logging, log_level, None) +if not isinstance(numeric_level, int): + raise ValueError("Invalid log level: %s" % log_level) +app.logger.setLevel(numeric_level) # Init database app.config["SQLALCHEMY_DATABASE_URI"] = config.db_read_sql_alchemy_database_config.get_sql_alchemy_uri() app.config["SQLALCHEMY_BINDS"] = { diff --git a/api/app/deposit_to_l2/__init__.py b/indexer/modules/custom/deposit_to_l2/endpoint/__init__.py similarity index 100% rename from api/app/deposit_to_l2/__init__.py rename to indexer/modules/custom/deposit_to_l2/endpoint/__init__.py diff --git a/api/app/deposit_to_l2/routes.py b/indexer/modules/custom/deposit_to_l2/endpoint/routes.py similarity index 52% rename from api/app/deposit_to_l2/routes.py rename to indexer/modules/custom/deposit_to_l2/endpoint/routes.py index d2d7c0f5e..45c8c95dc 100644 --- a/api/app/deposit_to_l2/routes.py +++ b/indexer/modules/custom/deposit_to_l2/endpoint/routes.py @@ -1,8 +1,10 @@ from time import time +from typing import Any, Dict, Optional import flask from flask_restx import Resource +from api.app.address.features import register_feature from api.app.cache import cache from api.app.db_service.af_token_deposit import ( get_deposit_assets_list, @@ -13,12 +15,12 @@ ) from api.app.db_service.blocks import get_block_by_hash from api.app.db_service.tokens import get_token_price_map_by_symbol_list -from api.app.deposit_to_l2 import token_deposit_namespace from api.app.utils.parse_utils import parse_deposit_assets, parse_deposit_transactions from common.utils.config import get_config from common.utils.exception_control import APIError from common.utils.format_utils import row_to_dict from common.utils.web3_utils import SUPPORT_CHAINS, chain_id_name_mapping +from indexer.modules.custom.deposit_to_l2.endpoint import token_deposit_namespace from indexer.modules.custom.deposit_to_l2.models.af_token_deposits__transactions import AFTokenDepositsTransactions PAGE_SIZE = 10 @@ -28,6 +30,104 @@ app_config = get_config() +@register_feature("deposit_to_l2", "value") +def get_deposit_to_l2_value(address) -> Optional[Dict[str, Any]]: + deposit_count = get_transactions_cnt_by_wallet(address) + if deposit_count is None or deposit_count == 0: + return None + + chains = get_deposit_chain_list(address) + chain_list = [chain_id_name_mapping[row_to_dict(chain)["chain_id"]] for chain in chains] + + assets = get_deposit_assets_list(address) + + asset_list = parse_deposit_assets(assets) + + token_symbol_list = [] + for asset in asset_list: + token_symbol_list.append(asset["token_symbol"]) + + token_price_map = get_token_price_map_by_symbol_list(list(set(token_symbol_list))) + + total_value_usd = 0 + for asset in asset_list: + if asset["token_symbol"] in token_price_map: + amount_usd = float(asset["amount"]) * float(token_price_map[asset["token_symbol"]]) + asset["amount_usd"] = amount_usd + total_value_usd += amount_usd + + return { + "address": address, + "deposit_count": deposit_count, + "chain_list": chain_list, + "asset_list": asset_list, + "total_value_usd": total_value_usd, + } + + +@register_feature("deposit_to_l2", "events") +def get_deposit_to_l2_events( + address, limit=5, offset=0, chain=None, contract=None, token=None, block=None +) -> Optional[Dict[str, Any]]: + if address: + address = address.lower() + bytes_address = bytes.fromhex(address[2:]) + filter_condition = AFTokenDepositsTransactions.wallet_address == bytes_address + + elif chain: + if chain.isnumeric(): + filter_condition = AFTokenDepositsTransactions.chain_id == chain + else: + if chain not in SUPPORT_CHAINS: + raise APIError( + f"{chain} is not supported yet, it will coming soon.", + code=400, + ) + + chain_id = SUPPORT_CHAINS[chain]["chain_id"] + filter_condition = AFTokenDepositsTransactions.chain_id == chain_id + + elif contract: + contract = contract.lower() + bytes_contract = bytes.fromhex(contract[2:]) + filter_condition = AFTokenDepositsTransactions.contract == bytes_contract + + elif token: + token = token.lower() + bytes_token = bytes.fromhex(token[2:]) + filter_condition = AFTokenDepositsTransactions.token == bytes_token + + elif block: + if block.isnumeric(): + filter_condition = AFTokenDepositsTransactions.block_number == int(block) + else: + block_number = get_block_by_hash(hash=block, columns=["number"]) + filter_condition = AFTokenDepositsTransactions.block_number == block_number + + transactions = get_transactions_by_condition( + filter_condition=filter_condition, + columns=[ + "transaction_hash", + "wallet_address", + "chain_id", + "contract_address", + "token_address", + "value", + "block_number", + "block_timestamp", + ], + limit=limit, + offset=offset, + ) + + total_records = get_transactions_cnt_by_condition(filter_condition=filter_condition) + transaction_list = parse_deposit_transactions(transactions) + + if total_records == 0: + return None + return {"data": transaction_list, "total": total_records} + + @token_deposit_namespace.route("/v1/aci/
/deposit_to_l2/transactions") class ACIDepositToL2Transactions(Resource): def get(self, address): @@ -44,74 +144,15 @@ def get(self, address): token = flask.request.args.get("token", None) block = flask.request.args.get("block", None) - has_filter = False - if address or chain or contract or token or block: - has_filter = True - if page_index * page_size > MAX_TRANSACTION_WITH_CONDITION: - raise APIError( - f"Showing the last {MAX_TRANSACTION_WITH_CONDITION} records only", - code=400, - ) - - filter_condition = True - - if address: - address = address.lower() - bytes_address = bytes.fromhex(address[2:]) - filter_condition = AFTokenDepositsTransactions.wallet_address == bytes_address - - elif chain: - if chain.isnumeric(): - filter_condition = AFTokenDepositsTransactions.chain_id == chain - else: - if chain not in SUPPORT_CHAINS: - raise APIError( - f"{chain} is not supported yet, it will coming soon.", - code=400, - ) - - chain_id = SUPPORT_CHAINS[chain]["chain_id"] - filter_condition = AFTokenDepositsTransactions.chain_id == chain_id - - elif contract: - contract = contract.lower() - bytes_contract = bytes.fromhex(contract[2:]) - filter_condition = AFTokenDepositsTransactions.contract == bytes_contract - - elif token: - token = token.lower() - bytes_token = bytes.fromhex(token[2:]) - filter_condition = AFTokenDepositsTransactions.token == bytes_token - - elif block: - if block.isnumeric(): - filter_condition = AFTokenDepositsTransactions.block_number == int(block) - else: - block_number = get_block_by_hash(hash=block, columns=["number"]) - filter_condition = AFTokenDepositsTransactions.block_number == block_number - - transactions = get_transactions_by_condition( - filter_condition=filter_condition, - columns=[ - "transaction_hash", - "wallet_address", - "chain_id", - "contract_address", - "token_address", - "value", - "block_number", - "block_timestamp", - ], + return get_deposit_to_l2_events( + address, limit=page_size, offset=(page_index - 1) * page_size, - ) - - total_records = get_transactions_cnt_by_condition(filter_condition=filter_condition) - transaction_list = parse_deposit_transactions(transactions) - - return { - "data": transaction_list, - "total": total_records, + chain=chain, + contract=contract, + token=token, + block=block, + ) or {"data": [], "total": 0} | { "page": page_index, "size": page_size, } @@ -126,35 +167,7 @@ def get(self, address): code=400, ) - deposit_count = get_transactions_cnt_by_wallet(address) - - chains = get_deposit_chain_list(address) - chain_list = [chain_id_name_mapping[row_to_dict(chain)["chain_id"]] for chain in chains] - - assets = get_deposit_assets_list(address) - - asset_list = parse_deposit_assets(assets) - - token_symbol_list = [] - for asset in asset_list: - token_symbol_list.append(asset["token_symbol"]) - - token_price_map = get_token_price_map_by_symbol_list(list(set(token_symbol_list))) - - total_value_usd = 0 - for asset in asset_list: - if asset["token_symbol"] in token_price_map: - amount_usd = float(asset["amount"]) * float(token_price_map[asset["token_symbol"]]) - asset["amount_usd"] = amount_usd - total_value_usd += amount_usd - - return { - "address": address, - "deposit_count": deposit_count, - "chain_list": chain_list, - "asset_list": asset_list, - "total_value_usd": total_value_usd, - } + return get_deposit_to_l2_value(address) @token_deposit_namespace.route("/v1/aci//deposit_to_l2/bridge_times") diff --git a/indexer/modules/custom/hemera_ens/endpoint/__init__.py b/indexer/modules/custom/hemera_ens/endpoint/__init__.py new file mode 100644 index 000000000..57bb18083 --- /dev/null +++ b/indexer/modules/custom/hemera_ens/endpoint/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +from flask_restx.namespace import Namespace + +af_ens_namespace = Namespace("Hemera ENS Namespace", path="/", description="ENS feature") diff --git a/api/app/af_ens/action_types.py b/indexer/modules/custom/hemera_ens/endpoint/action_types.py similarity index 100% rename from api/app/af_ens/action_types.py rename to indexer/modules/custom/hemera_ens/endpoint/action_types.py diff --git a/indexer/modules/custom/hemera_ens/endpoint/routes.py b/indexer/modules/custom/hemera_ens/endpoint/routes.py new file mode 100644 index 000000000..634dc2cf7 --- /dev/null +++ b/indexer/modules/custom/hemera_ens/endpoint/routes.py @@ -0,0 +1,309 @@ +import decimal +from copy import deepcopy +from datetime import date, datetime +from typing import Any, Dict, Optional + +import flask +from flask_restx import Resource +from sqlalchemy.sql import and_, or_ +from web3 import Web3 + +from api.app.address.features import register_feature +from common.models import db +from common.models.current_token_balances import CurrentTokenBalances +from common.models.erc721_token_id_details import ERC721TokenIdDetails +from common.models.erc721_token_transfers import ERC721TokenTransfers +from common.models.erc1155_token_transfers import ERC1155TokenTransfers +from common.utils.config import get_config +from common.utils.exception_control import APIError +from indexer.modules.custom.hemera_ens.endpoint import af_ens_namespace +from indexer.modules.custom.hemera_ens.endpoint.action_types import OperationType +from indexer.modules.custom.hemera_ens.models.af_ens_address_current import ENSAddress +from indexer.modules.custom.hemera_ens.models.af_ens_event import ENSMiddle +from indexer.modules.custom.hemera_ens.models.af_ens_node_current import ENSRecord + +app_config = get_config() + +w3 = Web3(Web3.HTTPProvider("https://ethereum-rpc.publicnode.com")) + +PAGE_SIZE = 10 + + +@register_feature("ens", "value") +def get_ens_current(address) -> Optional[Dict[str, Any]]: + if isinstance(address, str): + address = bytes.fromhex(address[2:]) + + res = { + "primary_name": None, + "resolve_to_names": [], + "ens_holdings": [], + "ens_holdings_total": 0, + "first_register_time": None, + "first_set_primary_time": None, + "resolve_to_names_total": 0, + } + origin_res = deepcopy(res) + + dn = datetime.now() + # current_address holds 721 & 1155 tokens + ens_token_address = bytes.fromhex("57F1887A8BF19B14FC0DF6FD9B2ACC9AF147EA85") + all_721_owns = ( + db.session.query(ERC721TokenIdDetails) + .filter( + and_(ERC721TokenIdDetails.token_owner == address, ERC721TokenIdDetails.token_address == ens_token_address) + ) + .all() + ) + all_721_ids = [r.token_id for r in all_721_owns] + all_owned = ( + db.session.query(ENSRecord) + .filter(and_(ENSRecord.token_id.in_(all_721_ids), ENSRecord.w_token_id.is_(None))) + .all() + ) + all_owned_map = {r.token_id: r for r in all_owned} + for id in all_721_ids: + r = all_owned_map.get(id) + res["ens_holdings"].append( + { + "name": r.name if r else None, + "is_expire": r.expires < dn if r and r.expires else True, + "type": "ERC721", + "token_id": str(id), + } + ) + ens_1155_address = bytes.fromhex("d4416b13d2b3a9abae7acd5d6c2bbdbe25686401") + all_1155_owns = ( + db.session.query(CurrentTokenBalances) + .filter(and_(CurrentTokenBalances.address == address, CurrentTokenBalances.token_address == ens_1155_address)) + .all() + ) + all_1155_ids = [r.token_id for r in all_1155_owns] + all_owned_1155_ens = ( + db.session.query(ENSRecord).filter(and_(ENSRecord.expires >= dn, ENSRecord.w_token_id.in_(all_1155_ids))).all() + ) + res["ens_holdings"].extend( + [ + {"name": r.name, "is_expire": r.expires < dn, "type": "ERC1155", "token_id": str(r.w_token_id)} + for r in all_owned_1155_ens + if r.name and r.name.endswith(".eth") + ] + ) + res["ens_holdings_total"] = len(res["ens_holdings"]) + primary_address_row = db.session.query(ENSAddress).filter(ENSAddress.address == address).first() + if primary_address_row: + res["primary_name"] = primary_address_row.name + # else: + # res["primary_name"] = w3.ens.name(w3.to_checksum_address(w3.to_hex(address))) + + be_resolved_ens = db.session.query(ENSRecord).filter(and_(ENSRecord.address == address)).all() + res["resolve_to_names"] = [ + { + "name": r.name, + "is_expire": r.expires < dn if r and r.expires else True, + } + for r in be_resolved_ens + if r.name and r.name.endswith(".eth") + ] + res["resolve_to_names_total"] = len(res["resolve_to_names"]) + + first_register = ( + db.session.query(ENSMiddle) + .filter( + and_( + ENSMiddle.from_address == address, + or_(ENSMiddle.event_name == "NameRegistered", ENSMiddle.event_name == "HashRegistered"), + ) + ) + .order_by(ENSMiddle.block_number) + .first() + ) + if first_register: + res["first_register_time"] = datetime_to_string(first_register.block_timestamp) + first_set_name = ( + db.session.query(ENSMiddle) + .filter( + and_( + ENSMiddle.from_address == address, + or_(ENSMiddle.method == "setName", ENSMiddle.event_name == "NameChanged"), + ) + ) + .order_by(ENSMiddle.block_number) + .first() + ) + if first_set_name: + res["first_set_primary_time"] = datetime_to_string(first_set_name.block_timestamp) + + return res if res != origin_res else None + + +@register_feature("ens", "events") +def get_ens_events(address, limit=5, offset=0) -> Optional[Dict[str, Any]]: + if isinstance(address, str): + address = bytes.fromhex(address[2:]) + events = [] + all_records_rows = ( + db.session.query(ENSMiddle) + .filter(ENSMiddle.from_address == address) + .order_by(ENSMiddle.block_number.desc(), ENSMiddle.log_index.desc()) + .limit(limit) + .offset(offset) + .all() + ) + # erc721_ids = list({r.token_id for r in all_records_rows if r.token_id}) + # erc721_id_transfers = ( + # db.session.query(ERC721TokenTransfers) + # .filter(ERC721TokenTransfers.token_id.in_(erc721_ids)) + # .order_by(ERC721TokenTransfers.block_number) + # .all() + # ) + # + # erc1155_ids = list({r.w_token_id for r in all_records_rows if r.w_token_id}) + # erc1155_id_transfers = ( + # db.session.query(ERC1155TokenTransfers) + # .filter(ERC1155TokenTransfers.token_id.in_(erc1155_ids)) + # .order_by(ERC1155TokenTransfers.block_number) + # .all() + # ) + + node_name_map = {} + for r in all_records_rows: + if r.name: + if r.name.endswith(".eth"): + node_name_map[r.node] = r.name + else: + node_name_map[r.node] = r.name + ".eth" + token_id_name_map = dict() + for r in all_records_rows: + if r.token_id: + token_id_name_map[r.token_id] = node_name_map[r.node] + if r.w_token_id: + token_id_name_map[r.w_token_id] = node_name_map[r.node] + + all_rows = merge_ens_middle(all_records_rows) + + for r in all_rows: + name = None + if hasattr(r, "node") and r.node and r.node in node_name_map: + name = node_name_map[r.node] + elif hasattr(r, "token_id") and r.token_id and r.token_id in token_id_name_map: + name = token_id_name_map[r.token_id] + elif hasattr(r, "w_token_id") and r.w_token_id and r.w_token_id in token_id_name_map: + name = token_id_name_map[r.w_token_id] + base = { + "block_number": r.block_number, + "block_timestamp": datetime_to_string(r.block_timestamp), + "transaction_hash": "0x" + r.transaction_hash.hex(), + "name": name, + } + + extras = get_action_type(r) + base.update(extras) + events.append(base) + + if len(events) == 0: + return None + + return {"data": events, "total": len(events)} + + +@af_ens_namespace.route("/v1/aci/
/ens/current") +class ACIEnsCurrent(Resource): + def get(self, address): + try: + address = bytes.fromhex(address[2:]) + except Exception: + raise APIError("Invalid Address Format", code=400) + return get_ens_current(address) + + +@af_ens_namespace.route("/v1/aci/
/ens/detail") +class ACIEnsDetail(Resource): + def get(self, address): + try: + address = bytes.fromhex(address[2:]) + except Exception: + raise APIError("Invalid Address Format", code=400) + + page_index = int(flask.request.args.get("page", 1)) + page_size = int(flask.request.args.get("size", PAGE_SIZE)) + limit = page_size + offset = (page_index - 1) * page_size + return get_ens_events(address, limit, offset) or {"data": [], "total": 0} | { + "page": page_index, + "size": page_size, + } + + +def merge_ens_middle(records): + """Merge consecutive records when setAddr and nameRegistered are duplicated""" + if not records: + return [] + + res = [records[0]] + + for current_record in records[1:]: + previous_record = res[-1] + + # Check if the current record should be merged with the previous one + if ( + current_record.event_name == previous_record.event_name == "AddressChanged" + and current_record.name == previous_record.name + ) or ( + current_record.event_name == previous_record.event_name == "NameRegistered" + and current_record.name == previous_record.name + ): + for column in ENSMiddle.__table__.columns: + current_value = getattr(current_record, column.name) + previous_value = getattr(previous_record, column.name) + if previous_value is None and current_value is not None: + setattr(previous_record, column.name, current_value) + else: + res.append(current_record) + + return res + + +def get_action_type(record): + if isinstance(record, ERC721TokenTransfers) or isinstance(record, ERC1155TokenTransfers): + return { + "action_type": OperationType.TRANSFER.value, + "from": "0x" + record.from_address.hex(), + "to": "0x" + record.to_address.hex(), + "token_id": int(record.token_id), + } + if record.method == "setName" or record.event_name == "NameChanged": + return {"action_type": OperationType.SET_PRIMARY_NAME.value} + if record.event_name == "NameRegistered" or record.event_name == "HashRegistered": + return {"action_type": OperationType.REGISTER.value} + if record.event_name == "NameRenewed": + return {"action_type": OperationType.RENEW.value, "expires": datetime_to_string(record.expires)} + if record.event_name == "AddressChanged": + return {"action_type": OperationType.SET_RESOLVED_ADDRESS.value, "address": "0x" + record.address.hex()} + raise ValueError("Unknown operation type") + + +def datetime_to_string(dt, format="%Y-%m-%d %H:%M:%S"): + if isinstance(dt, datetime): + return dt.astimezone().isoformat("T", "seconds") + elif isinstance(dt, date): + return dt.astimezone().isoformat("T", "seconds") + elif dt is None: + return None + else: + raise ValueError(f"Unsupported type for dt: {type(dt)}. Expected datetime or date.") + + +def model_to_dict(instance): + res = {} + for c in instance.__table__.columns: + v = getattr(instance, c.name) + if isinstance(v, datetime): + res[c.name] = v.isoformat() + elif isinstance(v, bytes): + res[c.name] = "0x" + v.hex() + elif isinstance(v, decimal.Decimal): + res[c.name] = str(v) + else: + res[c.name] = v + return res diff --git a/indexer/modules/custom/opensea/endpoint/routes.py b/indexer/modules/custom/opensea/endpoint/routes.py index 7c39645fa..4232cff23 100644 --- a/indexer/modules/custom/opensea/endpoint/routes.py +++ b/indexer/modules/custom/opensea/endpoint/routes.py @@ -1,16 +1,16 @@ from datetime import date, datetime from functools import lru_cache -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Optional, Union from flask import request from flask_restx import Resource -from sqlalchemy import and_, case, desc, func +from sqlalchemy import and_, desc, func +from api.app.address.features import register_feature from api.app.cache import cache from common.models import db from common.models.token_hourly_price import TokenHourlyPrices from common.models.tokens import Tokens -from common.utils.exception_control import APIError from common.utils.format_utils import as_dict, format_to_dict from indexer.modules.custom.opensea.endpoint import opensea_namespace from indexer.modules.custom.opensea.models.address_opensea_profile import AddressOpenseaProfile @@ -27,7 +27,8 @@ PAGE_SIZE = 10 -def get_opensea_profile(address: Union[str, bytes]) -> dict: +@register_feature("opensea", "value") +def get_opensea_profile(address: Union[str, bytes]) -> Optional[Dict[str, Any]]: """ Fetch and combine OpenSea profile data from both the profile table and recent transactions. """ @@ -35,7 +36,7 @@ def get_opensea_profile(address: Union[str, bytes]) -> dict: profile = db.session.query(AddressOpenseaProfile).filter_by(address=address_bytes).first() if not profile: - return {} + return None profile_data = as_dict(profile) @@ -49,6 +50,24 @@ def get_opensea_profile(address: Union[str, bytes]) -> dict: return profile_data | get_latest_opensea_transaction_by_address(address) +@register_feature("opensea", "events") +def get_opensea_events(address: Union[str, bytes], limit=5, offest=0) -> Optional[Dict[str, Any]]: + opensea_transactions = get_opensea_transactions_by_address( + address, + limit=limit, + offset=offest, + ) + total_count = get_opensea_address_order_cnt(address) + transaction_list = parse_opensea_order_transactions(opensea_transactions) + if total_count == 0: + return None + + return { + "data": transaction_list, + "total": total_count, + } + + def get_recent_opensea_transactions(address: bytes, timestamp: datetime) -> Dict[str, int]: """ Fetch recent OpenSea transactions data for a given address. @@ -319,22 +338,7 @@ def get(self, address): page_index = int(request.args.get("page", 1)) page_size = int(request.args.get("size", PAGE_SIZE)) - opensea_transactions = get_opensea_transactions_by_address( - address, - limit=page_size, - offset=(page_index - 1) * page_size, - ) - - if len(opensea_transactions) < page_size: - total_count = len(opensea_transactions) - else: - total_count = get_opensea_address_order_cnt(address) - - transaction_list = parse_opensea_order_transactions(opensea_transactions) - - return { - "data": transaction_list, - "total": total_count, + return get_opensea_events(address, page_size, (page_index - 1) * page_size) or {"data": [], "total": 0} | { "page": page_index, "size": page_size, } diff --git a/indexer/modules/custom/opensea/opensea_job.py b/indexer/modules/custom/opensea/opensea_job.py index ef2f3203e..c1597b82a 100644 --- a/indexer/modules/custom/opensea/opensea_job.py +++ b/indexer/modules/custom/opensea/opensea_job.py @@ -147,14 +147,14 @@ def transfer(self, opensea_logs: List[OpenseaLog]): consideration = calculate_total_amount(opensea_log.consideration) fee = calculate_fee_amount(opensea_log.consideration, opensea_log.fee_addresses) - opensea_transaciton_type = get_opensea_transaction_type(opensea_log.offer, opensea_log.consideration) + opensea_transaction_type = get_opensea_transaction_type(opensea_log.offer, opensea_log.consideration) if opensea_log.offerer == opensea_log.recipient: continue yield AddressOpenseaTransaction( address=opensea_log.offerer, related_address=opensea_log.recipient, is_offer=True, - transaction_type=opensea_transaciton_type.value, + transaction_type=opensea_transaction_type.value, order_hash=opensea_log.orderHash, zone=opensea_log.zone, offer=offer, @@ -172,9 +172,9 @@ def transfer(self, opensea_logs: List[OpenseaLog]): related_address=opensea_log.offerer, is_offer=False, transaction_type=( - 1 - opensea_transaciton_type.value - if opensea_transaciton_type.value <= 1 - else opensea_transaciton_type.value + 1 - opensea_transaction_type.value + if opensea_transaction_type.value <= 1 + else opensea_transaction_type.value ), order_hash=opensea_log.orderHash, zone=opensea_log.zone, diff --git a/indexer/modules/custom/uniswap_v3/endpoints/routes.py b/indexer/modules/custom/uniswap_v3/endpoints/routes.py index 9c058fae7..74d59030b 100644 --- a/indexer/modules/custom/uniswap_v3/endpoints/routes.py +++ b/indexer/modules/custom/uniswap_v3/endpoints/routes.py @@ -1,11 +1,13 @@ import math from datetime import datetime from time import time +from typing import Any, Dict, Optional from flask import request from flask_restx import Resource from sqlalchemy.sql import select, tuple_ +from api.app.address.features import register_feature from api.app.db_service.tokens import get_token_price_map_by_symbol_list from common.models import db from common.models.tokens import Tokens @@ -32,60 +34,114 @@ } -@uniswap_v3_namespace.route("/v1/aci/
/uniswap_v3_trading/swaps") -class UniswapV3WalletTradingRecords(Resource): - def get(self, address): - address = address.lower() - address_bytes = bytes.fromhex(address[2:]) - - swaps = ( - db.session.execute( - select(UniswapV3PoolSwapRecords) - .where(UniswapV3PoolSwapRecords.transaction_from_address == address_bytes) - .order_by(UniswapV3PoolSwapRecords.block_timestamp.desc()) - .limit(PAGE_SIZE) - ) - .scalars() - .all() +@register_feature("uniswap_v3_trading", "value") +def get_uniswap_v3_trading_value(address) -> Optional[Dict[str, Any]]: + if isinstance(address, str): + address = bytes.fromhex(address[2:]) + swaps = db.session.execute( + select( + UniswapV3PoolSwapRecords.transaction_hash, + UniswapV3PoolSwapRecords.token0_address, + UniswapV3PoolSwapRecords.token1_address, + UniswapV3PoolSwapRecords.amount0, + UniswapV3PoolSwapRecords.amount1, + ) + .where(UniswapV3PoolSwapRecords.transaction_from_address == address) + .order_by(UniswapV3PoolSwapRecords.block_timestamp.desc()) + ).all() + + token_list = [] + transaction_hash_list = [] + + for swap in swaps: + token_list.append(swap.token0_address) + token_list.append(swap.token1_address) + transaction_hash_list.append(swap.transaction_hash) + + token_list = list(set(token_list)) + transaction_hash_list = list(set(transaction_hash_list)) + if len(transaction_hash_list) == 0 or len(token_list) == 0: + return None + return { + "trade_count": len(transaction_hash_list), + "trade_asset_count": len(token_list), + "total_volume_usd": 0, + "average_value_usd": 0, + } + + +@register_feature("uniswap_v3_trading", "events") +def get_uniswap_v3_trading_events(address, limit=5, offset=0) -> Optional[Dict[str, Any]]: + if isinstance(address, str): + address = bytes.fromhex(address[2:]) + total_count = UniswapV3PoolSwapRecords.query.where( + UniswapV3PoolSwapRecords.transaction_from_address == address + ).count() + if total_count == 0: + return None + + swaps = ( + db.session.execute( + select(UniswapV3PoolSwapRecords) + .where(UniswapV3PoolSwapRecords.transaction_from_address == address) + .order_by(UniswapV3PoolSwapRecords.block_timestamp.desc()) + .limit(limit) + .offset(offset) + ) + .scalars() + .all() + ) + swap_records = [] + token_list = [] + for swap in swaps: + token_list.append(swap.token0_address) + token_list.append(swap.token1_address) + + tokens = db.session.execute(select(Tokens).where(Tokens.address.in_(list(set(token_list))))).scalars().all() + + token_map = {token.address: token for token in tokens} + + for swap in swaps: + token0 = token_map.get(swap.token0_address) + token1 = token_map.get(swap.token1_address) + swap_records.append( + { + "block_number": swap.block_number, + "block_timestamp": datetime.fromtimestamp(swap.block_timestamp).isoformat("T", "seconds"), + "transaction_hash": "0x" + swap.transaction_hash.hex(), + "pool_address": "0x" + swap.pool_address.hex(), + "amount0": "{0:.18f}".format(abs(swap.amount0) / 10**token0.decimals).rstrip("0").rstrip("."), + "amount1": "{0:.18f}".format(abs(swap.amount1) / 10**token1.decimals).rstrip("0").rstrip("."), + "token0_address": "0x" + swap.token0_address.hex(), + "token0_symbol": token0.symbol, + "token0_name": token0.name, + "token0_icon_url": token0.icon_url, + "token1_address": "0x" + swap.token1_address.hex(), + "token1_symbol": token1.symbol, + "token1_name": token1.name, + "token1_icon_url": token1.icon_url, + "action_type": get_swap_action_type(swap), + } ) - swap_records = [] - token_list = [] - for swap in swaps: - token_list.append(swap.token0_address) - token_list.append(swap.token1_address) - - tokens = db.session.execute(select(Tokens).where(Tokens.address.in_(list(set(token_list))))).scalars().all() - token_map = {token.address: token for token in tokens} + return { + "data": swap_records, + "total": total_count, + } - for swap in swaps: - token0 = token_map.get(swap.token0_address) - token1 = token_map.get(swap.token1_address) - swap_records.append( - { - "block_number": swap.block_number, - "block_timestamp": datetime.fromtimestamp(swap.block_timestamp).isoformat("T", "seconds"), - "transaction_hash": "0x" + swap.transaction_hash.hex(), - "pool_address": "0x" + swap.pool_address.hex(), - "amount0": "{0:.18f}".format(abs(swap.amount0) / 10**token0.decimals).rstrip("0").rstrip("."), - "amount1": "{0:.18f}".format(abs(swap.amount1) / 10**token1.decimals).rstrip("0").rstrip("."), - "token0_address": "0x" + swap.token0_address.hex(), - "token0_symbol": token0.symbol, - "token0_name": token0.name, - "token0_icon_url": token0.icon_url, - "token1_address": "0x" + swap.token1_address.hex(), - "token1_symbol": token1.symbol, - "token1_name": token1.name, - "token1_icon_url": token1.icon_url, - "action_type": get_swap_action_type(swap), - } - ) - return { - "data": swap_records, - "total": len(swap_records), - "page": 1, - "szie": PAGE_SIZE, +@uniswap_v3_namespace.route("/v1/aci/
/uniswap_v3_trading/swaps") +class UniswapV3WalletTradingRecords(Resource): + def get(self, address): + address = address.lower() + page_index = int(request.args.get("page", 1)) + page_size = int(request.args.get("size", PAGE_SIZE)) + + return ( + get_uniswap_v3_trading_events(address, page_size, (page_index - 1) * page_size) or {"data": [], "total": 0} + ) | { + "page": page_index, + "size": page_size, } @@ -93,39 +149,252 @@ def get(self, address): class UniswapV3WalletTradingSummary(Resource): def get(self, address): address = address.lower() - address_bytes = bytes.fromhex(address[2:]) - - swaps = db.session.execute( - select( - UniswapV3PoolSwapRecords.transaction_hash, - UniswapV3PoolSwapRecords.token0_address, - UniswapV3PoolSwapRecords.token1_address, - UniswapV3PoolSwapRecords.amount0, - UniswapV3PoolSwapRecords.amount1, - ) - .where(UniswapV3PoolSwapRecords.transaction_from_address == address_bytes) - .order_by(UniswapV3PoolSwapRecords.block_timestamp.desc()) - ).all() - - token_list = [] - transaction_hash_list = [] - - for swap in swaps: - token_list.append(swap.token0_address) - token_list.append(swap.token1_address) - transaction_hash_list.append(swap.transaction_hash) - - token_list = list(set(token_list)) - transaction_hash_list = list(set(transaction_hash_list)) - - return { - "trade_count": len(transaction_hash_list), - "trade_asset_count": len(token_list), + return get_uniswap_v3_trading_value(address) or { + "trade_count": 0, + "trade_asset_count": 0, "total_volume_usd": 0, "average_value_usd": 0, } +@register_feature("uniswap_v3_liquidity", "value") +def get_uniswap_v3_liquidity_value(address) -> Optional[Dict[str, Any]]: + address = address.lower() + address_bytes = bytes.fromhex(address[2:]) + + # Get all LP holdings + holdings = ( + db.session.query(UniswapV3TokenCurrentStatus) + .filter(UniswapV3TokenCurrentStatus.wallet_address == address_bytes) + .filter(UniswapV3TokenCurrentStatus.liquidity > 0) + .all() + ) + + # Get Pool Price + unique_pool_addresses = {holding.pool_address for holding in holdings} + pool_prices = ( + db.session.query(UniswapV3PoolCurrentPrices) + .filter(UniswapV3PoolCurrentPrices.pool_address.in_(unique_pool_addresses)) + .all() + ) + pool_price_map = {} + for data in pool_prices: + pool_address = "0x" + data.pool_address.hex() + pool_price_map[pool_address] = data.sqrt_price_x96 + + # Get token id info + token_id_list = [(holding.position_token_address, holding.token_id) for holding in holdings] + tokenIds = ( + db.session.query(UniswapV3Tokens) + .filter(tuple_(UniswapV3Tokens.position_token_address, UniswapV3Tokens.token_id).in_(token_id_list)) + .all() + ) + token_id_infos = {} + for token in tokenIds: + position_token_address = "0x" + token.position_token_address.hex() + token_id = token.token_id + key = (position_token_address, token_id) + token_id_infos[key] = token + + # Get Token info + erc20_tokens = set() + pool_infos = {} + pools = db.session.query(UniswapV3Pools).filter(UniswapV3Pools.pool_address.in_(unique_pool_addresses)).all() + for data in pools: + pool_address = "0x" + data.pool_address.hex() + pool_infos[pool_address] = data + erc20_tokens.add(data.token0_address) + erc20_tokens.add(data.token1_address) + + erc20_datas = db.session.query(Tokens).filter(Tokens.address.in_(erc20_tokens)).all() + erc20_infos = {} + token_symbol_list = [] + for data in erc20_datas: + erc20_infos["0x" + data.address.hex()] = data + token_symbol_list.append(data.symbol) + + # Get Token Price + token_price_map = get_token_price_map_by_symbol_list(list(set(token_symbol_list))) + + result = [] + total_value_usd = 0 + for holding in holdings: + position_token_address = "0x" + holding.position_token_address.hex() + token_id = holding.token_id + pool_address = "0x" + holding.pool_address.hex() + sqrt_price = pool_price_map[pool_address] + token_id_info = token_id_infos[(position_token_address, token_id)] + pool_info = pool_infos[pool_address] + token0_address = "0x" + pool_info.token0_address.hex() + token1_address = "0x" + pool_info.token1_address.hex() + if token0_address in erc20_infos: + token0_info = erc20_infos[token0_address] + else: + token0_info = Tokens(symbol="None", decimals=18) + if token1_address in erc20_infos: + token1_info = erc20_infos[token1_address] + else: + token1_info = Tokens(symbol="None", decimals=18) + amount0_str, amount1_str = get_token_amounts( + holding.liquidity, + sqrt_price, + token_id_info.tick_lower, + token_id_info.tick_upper, + token0_info.decimals, + token1_info.decimals, + ) + token0_value_usd = float(amount0_str) * float(token_price_map.get(token0_info.symbol, 0)) + token1_value_usd = float(amount1_str) * float(token_price_map.get(token1_info.symbol, 0)) + total_value_usd += token0_value_usd + total_value_usd += token1_value_usd + result.append( + { + "pool_address": pool_address, + "position_token_address": position_token_address, + "token_id": str(token_id), + "block_timestamp": datetime.fromtimestamp(holding.block_timestamp).isoformat("T", "seconds"), + "token0": { + "token0_symbol": token0_info.symbol, + "token0_icon_url": token0_info.icon_url, + "token0_balance": amount0_str, + "token0_value_usd": token0_value_usd, + }, + "token1": { + "token1_symbol": token1_info.symbol, + "token1_icon_url": token1_info.icon_url, + "token1_balance": amount1_str, + "token1_value_usd": token1_value_usd, + }, + } + ) + if len(result) == 0: + return None + + return { + "pool_count": len(unique_pool_addresses), + "total_value_usd": total_value_usd, + } + + +@register_feature("uniswap_v3_liquidity", "events") +def get_uniswap_v3_liquidity_events(address) -> Optional[Dict[str, Any]]: + address = address.lower() + address_bytes = bytes.fromhex(address[2:]) + + # Get all LP holdings + holdings = ( + db.session.query(UniswapV3TokenCurrentStatus) + .filter(UniswapV3TokenCurrentStatus.wallet_address == address_bytes) + .filter(UniswapV3TokenCurrentStatus.liquidity > 0) + .all() + ) + + # Get Pool Price + unique_pool_addresses = {holding.pool_address for holding in holdings} + pool_prices = ( + db.session.query(UniswapV3PoolCurrentPrices) + .filter(UniswapV3PoolCurrentPrices.pool_address.in_(unique_pool_addresses)) + .all() + ) + pool_price_map = {} + for data in pool_prices: + pool_address = "0x" + data.pool_address.hex() + pool_price_map[pool_address] = data.sqrt_price_x96 + + # Get token id info + token_id_list = [(holding.position_token_address, holding.token_id) for holding in holdings] + tokenIds = ( + db.session.query(UniswapV3Tokens) + .filter(tuple_(UniswapV3Tokens.position_token_address, UniswapV3Tokens.token_id).in_(token_id_list)) + .all() + ) + token_id_infos = {} + for token in tokenIds: + position_token_address = "0x" + token.position_token_address.hex() + token_id = token.token_id + key = (position_token_address, token_id) + token_id_infos[key] = token + + # Get Token info + erc20_tokens = set() + pool_infos = {} + pools = db.session.query(UniswapV3Pools).filter(UniswapV3Pools.pool_address.in_(unique_pool_addresses)).all() + for data in pools: + pool_address = "0x" + data.pool_address.hex() + pool_infos[pool_address] = data + erc20_tokens.add(data.token0_address) + erc20_tokens.add(data.token1_address) + + erc20_datas = db.session.query(Tokens).filter(Tokens.address.in_(erc20_tokens)).all() + erc20_infos = {} + token_symbol_list = [] + for data in erc20_datas: + erc20_infos["0x" + data.address.hex()] = data + token_symbol_list.append(data.symbol) + + # Get Token Price + token_price_map = get_token_price_map_by_symbol_list(list(set(token_symbol_list))) + + result = [] + total_value_usd = 0 + for holding in holdings: + position_token_address = "0x" + holding.position_token_address.hex() + token_id = holding.token_id + pool_address = "0x" + holding.pool_address.hex() + sqrt_price = pool_price_map[pool_address] + token_id_info = token_id_infos[(position_token_address, token_id)] + pool_info = pool_infos[pool_address] + token0_address = "0x" + pool_info.token0_address.hex() + token1_address = "0x" + pool_info.token1_address.hex() + if token0_address in erc20_infos: + token0_info = erc20_infos[token0_address] + else: + token0_info = Tokens(symbol="None", decimals=18) + if token1_address in erc20_infos: + token1_info = erc20_infos[token1_address] + else: + token1_info = Tokens(symbol="None", decimals=18) + amount0_str, amount1_str = get_token_amounts( + holding.liquidity, + sqrt_price, + token_id_info.tick_lower, + token_id_info.tick_upper, + token0_info.decimals, + token1_info.decimals, + ) + token0_value_usd = float(amount0_str) * float(token_price_map.get(token0_info.symbol, 0)) + token1_value_usd = float(amount1_str) * float(token_price_map.get(token1_info.symbol, 0)) + total_value_usd += token0_value_usd + total_value_usd += token1_value_usd + result.append( + { + "pool_address": pool_address, + "position_token_address": position_token_address, + "token_id": str(token_id), + "block_timestamp": datetime.fromtimestamp(holding.block_timestamp).isoformat("T", "seconds"), + "token0": { + "token0_symbol": token0_info.symbol, + "token0_icon_url": token0_info.icon_url, + "token0_balance": amount0_str, + "token0_value_usd": token0_value_usd, + }, + "token1": { + "token1_symbol": token1_info.symbol, + "token1_icon_url": token1_info.icon_url, + "token1_balance": amount1_str, + "token1_value_usd": token1_value_usd, + }, + } + ) + if len(result) == 0: + return None + + return { + "data": result, + "total": len(result), + } + + @uniswap_v3_namespace.route("/v1/aci/
/uniswap_v3_liquidity/current_holding") class UniswapV3WalletLiquidityHolding(Resource): def get(self, address): From 8a7dc3c8923e086ec4c70e0812ef6b93cf6e1b6a Mon Sep 17 00:00:00 2001 From: li xiang Date: Sat, 12 Oct 2024 18:13:21 +0800 Subject: [PATCH 09/12] Remove weth default (#184) --- indexer/jobs/export_tokens_and_transfers_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/jobs/export_tokens_and_transfers_job.py b/indexer/jobs/export_tokens_and_transfers_job.py index 947f5f120..fbfab4dc0 100644 --- a/indexer/jobs/export_tokens_and_transfers_job.py +++ b/indexer/jobs/export_tokens_and_transfers_job.py @@ -127,7 +127,7 @@ def __init__(self, **kwargs): ) self._is_batch = kwargs["batch_size"] > 1 - self.weth_address = self.user_defined_config.get("weth_address") or "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + self.weth_address = self.user_defined_config.get("weth_address") self.filter_token_address = self.user_defined_config.get("filter_token_address") or [] def get_filter(self): From d1f6299a64e6569234f5780e21844e64cbb720c2 Mon Sep 17 00:00:00 2001 From: will0x0909 <166356797+will0x0909@users.noreply.github.com> Date: Sat, 12 Oct 2024 18:26:40 +0800 Subject: [PATCH 10/12] Karak job (#164) karak partly support --- enumeration/entity_type.py | 8 + indexer/modules/custom/karak/__init__.py | 7 + .../custom/karak/endpoints/__init__.py | 6 + .../modules/custom/karak/endpoints/routes.py | 6 + .../modules/custom/karak/export_karak_job.py | 291 ++++++++++++++++++ indexer/modules/custom/karak/karak_abi.py | 42 +++ indexer/modules/custom/karak/karak_conf.py | 32 ++ indexer/modules/custom/karak/karak_domain.py | 56 ++++ .../modules/custom/karak/models/__init__.py | 6 + .../karak/models/af_karak_address_current.py | 42 +++ .../custom/karak/models/af_karak_records.py | 44 +++ .../karak/models/af_karak_vault_token.py | 37 +++ indexer/utils/utils.py | 12 + 13 files changed, 589 insertions(+) create mode 100644 indexer/modules/custom/karak/__init__.py create mode 100644 indexer/modules/custom/karak/endpoints/__init__.py create mode 100644 indexer/modules/custom/karak/endpoints/routes.py create mode 100644 indexer/modules/custom/karak/export_karak_job.py create mode 100644 indexer/modules/custom/karak/karak_abi.py create mode 100644 indexer/modules/custom/karak/karak_conf.py create mode 100644 indexer/modules/custom/karak/karak_domain.py create mode 100644 indexer/modules/custom/karak/models/__init__.py create mode 100644 indexer/modules/custom/karak/models/af_karak_address_current.py create mode 100644 indexer/modules/custom/karak/models/af_karak_records.py create mode 100644 indexer/modules/custom/karak/models/af_karak_vault_token.py diff --git a/enumeration/entity_type.py b/enumeration/entity_type.py index fd6f28983..0ff4e9ef4 100644 --- a/enumeration/entity_type.py +++ b/enumeration/entity_type.py @@ -29,6 +29,7 @@ ENSNameRenewD, ENSRegisterD, ) +from indexer.modules.custom.karak.karak_domain import KarakActionD, KarakAddressCurrentD, KarakVaultTokenD from indexer.modules.custom.opensea.domain.address_opensea_transactions import AddressOpenseaTransaction from indexer.modules.custom.opensea.domain.opensea_order import OpenseaOrder from indexer.modules.custom.uniswap_v3.domain.feature_uniswap_v3 import ( @@ -66,6 +67,8 @@ class EntityType(IntFlag): ENS = 1 << 10 + KARAK = 1 << 11 + EIGEN_LAYER = 1 << 13 EXPLORER = EXPLORER_BASE | EXPLORER_TOKEN | EXPLORER_TRACE @@ -186,6 +189,11 @@ def generate_output_types(entity_types): yield AddressOpenseaTransaction yield OpenseaOrder + if entity_types & EntityType.KARAK: + yield KarakActionD + yield KarakVaultTokenD + yield KarakAddressCurrentD + if entity_types & EntityType.EIGEN_LAYER: yield EigenLayerActionD yield EigenLayerAddressCurrentD diff --git a/indexer/modules/custom/karak/__init__.py b/indexer/modules/custom/karak/__init__.py new file mode 100644 index 000000000..e873e8e55 --- /dev/null +++ b/indexer/modules/custom/karak/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/19 15:18 +# @Author will +# @File __init__.py.py +# @Brief +"""Currently, this job only support Deposit, StartWithDraw, FinishWithDraw, more events coming soon""" diff --git a/indexer/modules/custom/karak/endpoints/__init__.py b/indexer/modules/custom/karak/endpoints/__init__.py new file mode 100644 index 000000000..1637b939a --- /dev/null +++ b/indexer/modules/custom/karak/endpoints/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/19 15:18 +# @Author will +# @File __init__.py.py +# @Brief diff --git a/indexer/modules/custom/karak/endpoints/routes.py b/indexer/modules/custom/karak/endpoints/routes.py new file mode 100644 index 000000000..4bb159c17 --- /dev/null +++ b/indexer/modules/custom/karak/endpoints/routes.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/20 15:01 +# @Author will +# @File routes.py +# @Brief diff --git a/indexer/modules/custom/karak/export_karak_job.py b/indexer/modules/custom/karak/export_karak_job.py new file mode 100644 index 000000000..15c748b70 --- /dev/null +++ b/indexer/modules/custom/karak/export_karak_job.py @@ -0,0 +1,291 @@ +import logging +from collections import defaultdict +from typing import Any, Dict, List + +from eth_abi import decode +from eth_typing import Decodable +from sqlalchemy import func + +from common.utils.exception_control import FastShutdownError +from indexer.domain.transaction import Transaction +from indexer.executors.batch_work_executor import BatchWorkExecutor +from indexer.jobs import FilterTransactionDataJob +from indexer.modules.custom.karak.karak_abi import DEPOSIT_EVENT, FINISH_WITHDRAWAL_EVENT, START_WITHDRAWAL_EVENT +from indexer.modules.custom.karak.karak_conf import CHAIN_CONTRACT +from indexer.modules.custom.karak.karak_domain import ( + KarakActionD, + KarakAddressCurrentD, + KarakVaultTokenD, + karak_address_current_factory, +) +from indexer.modules.custom.karak.models.af_karak_address_current import AfKarakAddressCurrent +from indexer.modules.custom.karak.models.af_karak_vault_token import AfKarakVaultToken +from indexer.specification.specification import TopicSpecification, TransactionFilterByLogs +from indexer.utils.abi import bytes_to_hex_str, decode_log +from indexer.utils.utils import extract_eth_address + +logger = logging.getLogger(__name__) + + +class ExportKarakJob(FilterTransactionDataJob): + # transaction with its logs + dependency_types = [Transaction] + output_types = [KarakActionD, KarakVaultTokenD, KarakAddressCurrentD] + able_to_reorg = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self._batch_work_executor = BatchWorkExecutor( + kwargs["batch_size"], + kwargs["max_workers"], + job_name=self.__class__.__name__, + ) + + self._is_batch = kwargs["batch_size"] > 1 + self.db_service = kwargs["config"].get("db_service") + self.chain_id = self._web3.eth.chain_id + self.karak_conf = CHAIN_CONTRACT[self.chain_id] + self.token_vault = dict() + self.vault_token = dict() + self.init_vaults() + + def init_vaults(self): + # fetch from database + if not self.db_service: + return + + with self.db_service.get_service_session() as session: + query = session.query(AfKarakVaultToken) + result = query.all() + + for r in result: + self.token_vault[bytes_to_hex_str(r.token)] = bytes_to_hex_str(r.vault) + self.vault_token[bytes_to_hex_str(r.vault)] = bytes_to_hex_str(r.token) + logging.info(f"init vaults with {len(self.token_vault)} tokens") + + def get_filter(self): + # deposit, startWithdraw, finishWithdraw + topics = [] + addresses = [] + for k, item in self.karak_conf.items(): + if isinstance(item, dict) and item.get("topic"): + topics.append(item["topic"]) + if isinstance(item, dict) and item.get("address"): + addresses.append(item["address"]) + for ad in self.vault_token: + addresses.append(ad) + return [ + TransactionFilterByLogs(topics_filters=[TopicSpecification(topics=topics, addresses=addresses)]), + ] + + def discover_vaults(self, transactions: List[Transaction]): + res = [] + for transaction in transactions: + # deployVault + if not transaction.input.startswith(self.karak_conf["NEW_VAULT"]["starts_with"]): + continue + logs = transaction.receipt.logs + vault = None + for log in logs: + if ( + log.topic0 == self.karak_conf["NEW_VAULT"]["topic"] + and log.address == self.karak_conf["NEW_VAULT"]["address"] + ): + vault = extract_eth_address(log.topic1) + break + dd = self.decode_function( + ["address", "string", "string", "uint8"], bytes.fromhex(transaction.input[2:])[4:] + ) + kvt = KarakVaultTokenD( + vault=vault, + token=dd[0], + name=dd[1], + symbol=dd[2], + asset_type=dd[3], + ) + self.token_vault[kvt.token] = kvt.vault + self.vault_token[kvt.vault] = kvt.token + res.append(kvt) + return res + + def _collect(self, **kwargs): + transactions: List[Transaction] = self._data_buff.get(Transaction.type(), []) + new_vaults = self.discover_vaults(transactions) + if new_vaults: + self._collect_items(KarakVaultTokenD.type(), new_vaults) + res = [] + + for transaction in transactions: + logs = transaction.receipt.logs + for log in logs: + if log.topic0 == self.karak_conf["DEPOSIT"]["topic"] and log.address in self.vault_token: + dl = decode_log(DEPOSIT_EVENT, log) + vault = log.address + amount = dl.get("shares") + by = dl.get("by") + if not by: + staker = transaction.from_address + else: + staker = by + owner = dl.get("owner") + + if not amount or not vault: + raise FastShutdownError(f"karak job failed {transaction.hash}") + kad = KarakActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=DEPOSIT_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + vault=vault, + amount=amount, + staker=staker, + ) + res.append(kad) + elif ( + log.topic0 == self.karak_conf["START_WITHDRAW"]["topic"] + and log.address == self.karak_conf["START_WITHDRAW"]["address"] + ): + dl = decode_log(START_WITHDRAWAL_EVENT, log) + vault = dl.get("vault") + staker = dl.get("staker") + operator = dl.get("operator") + withdrawer = dl.get("withdrawer") + shares = dl.get("shares") + kad = KarakActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=START_WITHDRAWAL_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + vault=vault, + staker=staker, + operator=operator, + withdrawer=withdrawer, + shares=shares, + amount=shares, + ) + res.append(kad) + + elif ( + log.topic0 == self.karak_conf["FINISH_WITHDRAW"]["topic"] + and log.address == self.karak_conf["FINISH_WITHDRAW"]["address"] + ): + dl = decode_log(FINISH_WITHDRAWAL_EVENT, log) + vault = dl.get("vault") + staker = dl.get("staker") + operator = dl.get("operator") + withdrawer = dl.get("withdrawer") + shares = dl.get("shares") + withdrawroot = dl.get("withdrawRoot") + kad = KarakActionD( + transaction_hash=transaction.hash, + log_index=log.log_index, + transaction_index=transaction.transaction_index, + block_number=log.block_number, + block_timestamp=log.block_timestamp, + method=transaction.get_method_id(), + event_name=FINISH_WITHDRAWAL_EVENT["name"], + topic0=log.topic0, + from_address=transaction.from_address, + to_address=transaction.to_address, + vault=vault, + staker=staker, + operator=operator, + withdrawer=withdrawer, + shares=shares, + withdrawroot=withdrawroot, + amount=shares, + ) + res.append(kad) + self._collect_items(KarakActionD.type(), res) + batch_result_dic = self.calculate_batch_result(res) + exists_dic = self.get_existing_address_current(list(batch_result_dic.keys())) + for address, outer_dic in batch_result_dic.items(): + for vault, kad in outer_dic.items(): + if address in exists_dic and vault in exists_dic[address]: + exists_kad = exists_dic[address][vault] + exists_kad.deposit_amount += kad.deposit_amount + exists_kad.start_withdraw_amount += kad.start_withdraw_amount + exists_kad.finish_withdraw_amount += kad.finish_withdraw_amount + self._collect_item(kad.type(), exists_kad) + else: + self._collect_item(kad.type(), kad) + + @staticmethod + def decode_function(decode_types, output: Decodable) -> Any: + try: + return decode(decode_types, output) + except Exception as e: + logger.error(e) + return [None] * len(decode_types) + + def get_existing_address_current(self, addresses): + if not self.db_service: + return {} + + addresses = [ad[2:] for ad in addresses if ad and ad.startswith("0x")] + if not addresses: + return {} + with self.db_service.get_service_session() as session: + query = session.query(AfKarakAddressCurrent).filter( + func.encode(AfKarakAddressCurrent.address, "hex").in_(addresses) + ) + result = query.all() + lis = [] + for rr in result: + lis.append( + KarakAddressCurrentD( + address=bytes_to_hex_str(rr.address), + vault=bytes_to_hex_str(rr.vault), + deposit_amount=rr.deposit_amount, + start_withdraw_amount=rr.start_withdraw_amount, + finish_withdraw_amount=rr.finish_withdraw_amount, + ) + ) + + return create_nested_dict(lis) + + def calculate_batch_result(self, karak_actions: List[KarakActionD]) -> Any: + def nested_dict(): + return defaultdict(karak_address_current_factory) + + res_d = defaultdict(nested_dict) + for action in karak_actions: + staker = action.staker + vault = action.vault + topic0 = action.topic0 + if topic0 == self.karak_conf["DEPOSIT"]["topic"]: + res_d[staker][vault].address = staker + res_d[staker][vault].vault = vault + res_d[staker][vault].deposit_amount += action.amount + elif topic0 == self.karak_conf["START_WITHDRAW"]["topic"]: + res_d[staker][vault].address = staker + res_d[staker][vault].vault = vault + res_d[staker][vault].start_withdraw_amount += action.amount + elif topic0 == self.karak_conf["FINISH_WITHDRAW"]["topic"]: + res_d[staker][vault].address = staker + res_d[staker][vault].vault = vault + res_d[staker][vault].finish_withdraw_amount += action.amount + return res_d + + +def create_nested_dict(data_list: List[KarakAddressCurrentD]) -> Dict[str, Dict[str, KarakAddressCurrentD]]: + result = {} + for item in data_list: + if item.address and item.vault: + if item.address not in result: + result[item.address] = {} + result[item.address][item.vault] = item + return result diff --git a/indexer/modules/custom/karak/karak_abi.py b/indexer/modules/custom/karak/karak_abi.py new file mode 100644 index 000000000..d05a38a92 --- /dev/null +++ b/indexer/modules/custom/karak/karak_abi.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/20 11:07 +# @Author will +# @File karak_abi.py +# @Brief +import json +from typing import cast + +from web3.types import ABIEvent + +from indexer.utils.abi import event_log_abi_to_topic + +DEPOSIT_EVENT = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"by","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Deposit","type":"event"}""" + ), +) + +START_WITHDRAWAL_EVENT = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawer","type":"address"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"StartedWithdrawal","type":"event"} +""" + ), +) + +FINISH_WITHDRAWAL_EVENT = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawer","type":"address"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"withdrawRoot","type":"bytes32"}],"name":"FinishedWithdrawal","type":"event"} +""" + ), +) + +TRANSFER_EVENT = cast( + ABIEvent, + json.loads( + """{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Transfer","type":"event"}""" + ), +) diff --git a/indexer/modules/custom/karak/karak_conf.py b/indexer/modules/custom/karak/karak_conf.py new file mode 100644 index 000000000..64b8c4492 --- /dev/null +++ b/indexer/modules/custom/karak/karak_conf.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/23 13:31 +# @Author will +# @File karak_conf.py.py +# @Brief +CHAIN_CONTRACT = { + 1: { + "DEPOSIT": { + # address is all vaults + "topic": "0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7", + }, + "TRANSFER": { + # address is all vaults + "topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + }, + "START_WITHDRAW": { + "address": "0xafa904152e04abff56701223118be2832a4449e0", + "topic": "0x6ee63f530864567ac8a1fcce5050111457154b213c6297ffc622603e8497f7b2", + }, + "FINISH_WITHDRAW": { + "address": "0xafa904152e04abff56701223118be2832a4449e0", + "topic": "0x486508c3c40ef7985dcc1f7d43acb1e77e0059505d1f0e6064674ca655a0c82f", + }, + "NEW_VAULT": { + "address": "0x54e44dbb92dba848ace27f44c0cb4268981ef1cc", + "topic": "0x2cd7a531712f8899004c782d9607e0886d1dbc91bfac7be88dadf6750d9e1419", + "starts_with": "0xf0edf6aa", + }, + "VAULT_SUPERVISOR": "0x54e44dbb92dba848ace27f44c0cb4268981ef1cc", + } +} diff --git a/indexer/modules/custom/karak/karak_domain.py b/indexer/modules/custom/karak/karak_domain.py new file mode 100644 index 000000000..633b98c14 --- /dev/null +++ b/indexer/modules/custom/karak/karak_domain.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass +from typing import Optional + +from indexer.domain import FilterData + + +@dataclass +class KarakActionD(FilterData): + transaction_hash: str + log_index: int + transaction_index: int + block_number: Optional[int] = None + block_timestamp: Optional[int] = None + method: Optional[str] = None + event_name: Optional[str] = None + topic0: Optional[str] = None + from_address: Optional[str] = None + to_address: Optional[str] = None + + token: Optional[str] = None + vault: Optional[str] = None + amount: Optional[int] = None + balance: Optional[int] = None + staker: Optional[str] = None + operator: Optional[str] = None + withdrawer: Optional[str] = None + shares: Optional[int] = None + withdrawroot: Optional[str] = None + + +@dataclass +class KarakVaultTokenD(FilterData): + vault: Optional[str] = None + token: Optional[str] = None + name: Optional[str] = None + symbol: Optional[str] = None + asset_type: Optional[int] = None + + +@dataclass +class KarakAddressCurrentD(FilterData): + address: Optional[str] = None + vault: Optional[str] = None + deposit_amount: Optional[int] = None + start_withdraw_amount: Optional[int] = None + finish_withdraw_amount: Optional[int] = None + + +def karak_address_current_factory(): + return KarakAddressCurrentD( + address=None, + vault=None, + deposit_amount=0, + start_withdraw_amount=0, + finish_withdraw_amount=0, + ) diff --git a/indexer/modules/custom/karak/models/__init__.py b/indexer/modules/custom/karak/models/__init__.py new file mode 100644 index 000000000..1637b939a --- /dev/null +++ b/indexer/modules/custom/karak/models/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/19 15:18 +# @Author will +# @File __init__.py.py +# @Brief diff --git a/indexer/modules/custom/karak/models/af_karak_address_current.py b/indexer/modules/custom/karak/models/af_karak_address_current.py new file mode 100644 index 000000000..60fd68191 --- /dev/null +++ b/indexer/modules/custom/karak/models/af_karak_address_current.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/19 15:24 +# @Author will +# @File af_karak_address_current.py +# @Brief + +from sqlalchemy import Column, PrimaryKeyConstraint, func, text +from sqlalchemy.dialects.postgresql import BOOLEAN, BYTEA, NUMERIC, TIMESTAMP + +from common.models import HemeraModel, general_converter + + +class AfKarakAddressCurrent(HemeraModel): + __tablename__ = "af_karak_address_current" + address = Column(BYTEA, primary_key=True) + + vault = Column(BYTEA, primary_key=True) + deposit_amount = Column(NUMERIC(100)) + start_withdraw_amount = Column(NUMERIC(100)) + finish_withdraw_amount = Column(NUMERIC(100)) + + d_s = Column(NUMERIC(100)) + d_f = Column(NUMERIC(100)) + s_f = Column(NUMERIC(100)) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now()) + reorg = Column(BOOLEAN, server_default=text("false")) + + __table_args__ = (PrimaryKeyConstraint("address", "vault"),) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "KarakAddressCurrentD", + "conflict_do_update": True, + "update_strategy": None, + "converter": general_converter, + } + ] diff --git a/indexer/modules/custom/karak/models/af_karak_records.py b/indexer/modules/custom/karak/models/af_karak_records.py new file mode 100644 index 000000000..c04394811 --- /dev/null +++ b/indexer/modules/custom/karak/models/af_karak_records.py @@ -0,0 +1,44 @@ +from sqlalchemy import Column, Numeric, PrimaryKeyConstraint, func, text +from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, BYTEA, INTEGER, NUMERIC, TIMESTAMP, VARCHAR + +from common.models import HemeraModel, general_converter + + +class AfKarakRecords(HemeraModel): + __tablename__ = "af_karak_records" + transaction_hash = Column(BYTEA, primary_key=True) + log_index = Column(INTEGER, primary_key=True) + block_number = Column(BIGINT) + block_timestamp = Column(BIGINT) + method = Column(VARCHAR) + event_name = Column(VARCHAR) + topic0 = Column(VARCHAR) + from_address = Column(BYTEA) + to_address = Column(BYTEA) + + token = Column(VARCHAR) + vault = Column(BYTEA) + amount = Column(NUMERIC(100)) + balance = Column(NUMERIC(100)) + staker = Column(VARCHAR) + operator = Column(VARCHAR) + withdrawer = Column(VARCHAR) + shares = Column(Numeric(100)) + withdrawroot = Column(VARCHAR) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now()) + reorg = Column(BOOLEAN, server_default=text("false")) + + __table_args__ = (PrimaryKeyConstraint("transaction_hash", "log_index"),) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "KarakActionD", + "conflict_do_update": True, + "update_strategy": None, + "converter": general_converter, + } + ] diff --git a/indexer/modules/custom/karak/models/af_karak_vault_token.py b/indexer/modules/custom/karak/models/af_karak_vault_token.py new file mode 100644 index 000000000..f59f47d28 --- /dev/null +++ b/indexer/modules/custom/karak/models/af_karak_vault_token.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time 2024/9/20 10:11 +# @Author will +# @File af_karak_vault_token.py +# @Brief +from sqlalchemy import INT, VARCHAR, Column, PrimaryKeyConstraint, func, text +from sqlalchemy.dialects.postgresql import BOOLEAN, BYTEA, TIMESTAMP + +from common.models import HemeraModel, general_converter + + +class AfKarakVaultToken(HemeraModel): + __tablename__ = "af_karak_vault_token" + + vault = Column(BYTEA, primary_key=True, nullable=False) + token = Column(BYTEA, primary_key=True, nullable=False) + name = Column(VARCHAR) + symbol = Column(VARCHAR) + asset_type = Column(INT) + + create_time = Column(TIMESTAMP, server_default=func.now()) + update_time = Column(TIMESTAMP, server_default=func.now()) + reorg = Column(BOOLEAN, server_default=text("false")) + + __table_args__ = (PrimaryKeyConstraint("vault", "token"),) + + @staticmethod + def model_domain_mapping(): + return [ + { + "domain": "KarakVaultTokenD", + "conflict_do_update": True, + "update_strategy": None, + "converter": general_converter, + } + ] diff --git a/indexer/utils/utils.py b/indexer/utils/utils.py index b9f791eb1..34640d6e9 100644 --- a/indexer/utils/utils.py +++ b/indexer/utils/utils.py @@ -5,6 +5,8 @@ import warnings from typing import List, Optional, Union +from web3 import Web3 + from common.utils.exception_control import RetriableError, decode_response_error from indexer.domain import Domain @@ -164,6 +166,16 @@ def format_block_id(block_id: Union[Optional[int], str]) -> str: return hex(block_id) if block_id and isinstance(block_id, int) else block_id +def extract_eth_address(input_string): + hex_string = input_string.lower().replace("0x", "") + + if len(hex_string) > 40: + hex_string = hex_string[-40:] + + hex_string = hex_string.zfill(40) + return Web3.to_checksum_address(hex_string).lower() + + def flatten(lst): result = [] for item in lst: From 660e96bd688350833d5d8d71af2d849e4876942e Mon Sep 17 00:00:00 2001 From: li xiang Date: Mon, 14 Oct 2024 18:45:28 +0800 Subject: [PATCH 11/12] Update development environment docs (#185) --- README.md | 105 +++++++++++++++++++++++++++++++++-------- docs/README.md | 59 ++++++++++------------- indexer/utils/utils.py | 2 +- 3 files changed, 111 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 78f860726..68e05b877 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ As of July 5, 2024, the initial open-source version of the Hemera Indexer offers ## Features Offered -##### Export the following entities +#### Exportable Entities + +The system can export the following entities: - Blocks - Transactions @@ -33,13 +35,80 @@ As of July 5, 2024, the initial open-source version of the Hemera Indexer offers - DA Transactions - User Operations -##### Into the following formats +#### Supported Export Formats + +The data can be exported into the following formats: - Postgresql SQL - JSONL - CSV -##### Additional features +#### Output Types and Entity Types Explanation + +##### Entity Types + +Entity Types are high-level categories that group related data models. They are defined in the `EntityType` enum and can be combined using bitwise operations. + +##### Key Points: +- Specified using the `-E` or `--entity-types` option +- Examples: EXPLORER_BASE, EXPLORER_TOKEN, EXPLORER_TRACE, etc. +- Multiple types can be combined using commas + +##### Output Types + +Output Types correspond to more detailed data models and are typically associated with specific Entity Types. + +##### Key Points: +- Specified using the `-O` or `--output-types` option +- Examples: Block, Transaction, Log, Token, AddressTokenBalance, etc. +- Takes precedence over Entity Types if specified +- Directly corresponds to data class names in the code (Domain) + +##### Relationship between Entity Types and Output Types + +1. Entity Types are used to generate a set of Output Types: + - The `generate_output_types` function maps Entity Types to their corresponding Output Types. + - Each Entity Type yields a set of related data classes (Output Types). + +2. When specifying Output Types directly: + - It overrides the Entity Type selection. + - Allows for more granular control over the exported data. + +#### Output Types and Data Classes + +It's important to note that when using the `--output-types` option, you should specify the names that directly correspond to the data class names in the code. For example: + +``` +--output-types Block,Transaction,Log,Token,ERC20TokenTransfer +``` + +These names should match exactly with the data class definitions in your codebase. The Output Types are essentially the same as the data class names, allowing for precise selection of the data models you wish to export. + +#### Usage Examples + +1. Using Entity Types: + ``` + --entity-types EXPLORER_BASE,EXPLORER_TOKEN + ``` + This will generate Output Types including Block, Transaction, Log, Token, ERC20TokenTransfer, etc. + +2. Using Output Types: + ``` + --output-types Block,Transaction,Token + ``` + This will only generate the specified Output Types, regardless of Entity Types. + +#### Note + +When developing or using this system, consider the following: +- Entity Types provide a broader, category-based selection of data. +- Output Types offer more precise control over the exact data models to be exported. +- The choice between using Entity Types or Output Types depends on the specific requirements of the data export task. + + +These names should match exactly with the data class definitions in your codebase. The Output Types are essentially the same as the data class names, allowing for precise selection of the data models you wish to export. + +#### Additional features - Ability to select arbitrary block ranges for more flexible data indexing - Option to choose any entities for targeted data extraction @@ -232,7 +301,7 @@ Follow the instructions about how to set up a PostgreSQL database here: [Setup P Configure the `OUTPUT` or `--output` parameter according to your PostgreSQL role information. Check out [Configure Hemera Indexer](#output-or---output) for details. -E.g. `postgresql+psycopg2://${YOUR_USER}:${YOUR_PASSWORD}@${YOUR_HOST}:5432/${YOUR_DATABASE}`. +E.g. `postgresql://${YOUR_USER}:${YOUR_PASSWORD}@${YOUR_HOST}:5432/${YOUR_DATABASE}`. #### Run @@ -240,15 +309,14 @@ Please check out [Configure Hemera Indexer](#configure-hemera-indexer) on how to ```bash python hemera.py stream \ - --provider-uri https://eth.llamarpc.com \ - --debug-provider-uri https://eth.llamarpc.com \ - --postgres-url postgresql+psycopg2://devuser:devpassword@localhost:5432/hemera_indexer \ - --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql+psycopg2://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ + --provider-uri https://ethereum.publicnode.com \ + --postgres-url postgresql://devuser:devpassword@localhost:5432/hemera_indexer \ + --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ --start-block 20000001 \ --end-block 20010000 \ # alternatively you can spin up a separate process for traces, as it takes more time # --entity-types trace,contract,coin_balance - --entity-types block,transaction,log,token,token_transfer \ + --entity-types EXPLORER_BASE \ --block-batch-size 200 \ --batch-size 200 \ --max-workers 32 @@ -308,19 +376,18 @@ E.g., If you specify the `OUTPUT` or `--output` parameter as below ```bash # Command line parameter python hemera.py stream \ - --provider-uri https://eth.llamarpc.com \ - --debug-provider-uri https://eth.llamarpc.com \ - --postgres-url postgresql+psycopg2://devuser:devpassword@localhost:5432/hemera_indexer \ - --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql+psycopg2://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ + --provider-uri https://ethereum.publicnode.com \ + --postgres-url postgresql://devuser:devpassword@localhost:5432/hemera_indexer \ + --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ --start-block 20000001 \ --end-block 20010000 \ - --entity-types block,transaction,log,token,token_transfer \ + --entity-types EXPLORER_BASE \ --block-batch-size 200 \ --batch-size 200 \ --max-workers 32 # Or using environment variable -export OUTPUT = postgresql+psycopg2://user:password@localhost:5432/hemera_indexer,jsonfile://output/json, csvfile://output/csv +export OUTPUT = postgresql://user:password@localhost:5432/hemera_indexer,jsonfile://output/json, csvfile://output/csv ``` You will be able to find those results in the `output` folder of your current location. @@ -367,7 +434,7 @@ The URI of the web3 debug rpc provider, e.g. `file://$HOME/Library/Ethereum/geth #### `POSTGRES_URL` or `--postgres-url` or `-pg` [**Required**] -The PostgreSQL connection URL that the Hemera Indexer used to maintain its state. e.g. `postgresql+psycopg2://user:password@127.0.0.1:5432/postgres`. +The PostgreSQL connection URL that the Hemera Indexer used to maintain its state. e.g. `postgresql://user:password@127.0.0.1:5432/postgres`. #### `OUTPUT` or `--output` or `-o` @@ -379,19 +446,19 @@ The file location will be relative to your current location if you run from sour e.g. -- `postgresql+psycopg2://user:password@localhost:5432/hemera_indexer`: Output will be exported to your postgres. +- `postgresql://user:password@localhost:5432/hemera_indexer`: Output will be exported to your postgres. - `jsonfile://output/json`: Json files will be exported to folder `output/json` - `csvfile://output/csv`: Csv files will be exported to folder `output/csv` - `console,jsonfile://output/json,csvfile://output/csv`: Multiple destinations are supported. #### `ENTITY_TYPES` or `--entity-types` or `-E` -[**Default**: `EXPLORER_BASE,EXPLORER_TOKEN`] +[**Default**: `EXPLORER_BASE`] The list of entity types to export. e.g. `EXPLORER_BASE`, `EXPLORER_TOKEN`, `EXPLORER_TRACE`. #### `OUTPUT_TYPES` or `--output-types` or `-O` -The list of output types to export, corresponding to more detailed data models. Specifying this option will prioritize these settings over the entity types specified in -E. Available options include: block, transaction, log, token, address_token_balance, erc20_token_transfer, erc721_token_transfer, erc1155_token_transfer, trace, contract, coin_balance. +The list of output types to export, corresponding to more detailed data models. Specifying this option will prioritize these settings over the entity types specified in -E. Available options include: Block, Transaction, Log, Token, AddressTokenBalance, etc. You may spawn up multiple Hemera Indexer processes, each of them specifying different output types to accelerate the indexing process. For example, indexing `trace` data may take much longer than other entities, you may want to run a separate process to index `trace` data. Checkout `docker-compose/docker-compose.yaml` for examples. diff --git a/docs/README.md b/docs/README.md index 9ff99e6ca..f8b41916b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -227,7 +227,7 @@ Follow the instructions about how to set up a PostgreSQL database here: [Setup P Configure the `OUTPUT` or `--output` parameter according to your PostgreSQL role information. Check out [Configure Hemera Indexer](#output-or---output) for details. -E.g. `postgresql+psycopg2://${YOUR_USER}:${YOUR_PASSWORD}@${YOUR_HOST}:5432/${YOUR_DATABASE}`. +E.g. `postgresql://${YOUR_USER}:${YOUR_PASSWORD}@${YOUR_HOST}:5432/${YOUR_DATABASE}`. #### Run @@ -235,15 +235,14 @@ Please check out [Configure Hemera Indexer](#configure-hemera-indexer) on how to ```bash python hemera.py stream \ - --provider-uri https://eth.llamarpc.com \ - --debug-provider-uri https://eth.llamarpc.com \ - --postgres-url postgresql+psycopg2://devuser:devpassword@localhost:5432/hemera_indexer \ - --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql+psycopg2://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ + --provider-uri https://ethereum.publicnode.com \ + --postgres-url postgresql://devuser:devpassword@localhost:5432/hemera_indexer \ + --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ --start-block 20000001 \ --end-block 20010000 \ # alternatively you can spin up a separate process for traces, as it takes more time # --entity-types trace,contract,coin_balance - --entity-types block,transaction,log,token,token_transfer \ + --entity-types EXPLORER_BASE \ --block-batch-size 200 \ --batch-size 200 \ --max-workers 32 @@ -303,19 +302,18 @@ E.g., If you specify the `OUTPUT` or `--output` parameter as below ```bash # Command line parameter python hemera.py stream \ - --provider-uri https://eth.llamarpc.com \ - --debug-provider-uri https://eth.llamarpc.com \ - --postgres-url postgresql+psycopg2://devuser:devpassword@localhost:5432/hemera_indexer \ - --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql+psycopg2://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ + --provider-uri https://ethereum.publicnode.com \ + --postgres-url postgresql://devuser:devpassword@localhost:5432/hemera_indexer \ + --output jsonfile://output/eth_blocks_20000001_20010000/json,csvfile://output/hemera_indexer/csv,postgresql://devuser:devpassword@localhost:5432/eth_blocks_20000001_20010000 \ --start-block 20000001 \ --end-block 20010000 \ - --entity-types block,transaction,log,token,token_transfer \ + --entity-types EXPLORER_BASE \ --block-batch-size 200 \ --batch-size 200 \ --max-workers 32 # Or using environment variable -export OUTPUT = postgresql+psycopg2://user:password@localhost:5432/hemera_indexer,jsonfile://output/json, csvfile://output/csv +export OUTPUT = postgresql://user:password@localhost:5432/hemera_indexer,jsonfile://output/json, csvfile://output/csv ``` You will be able to find those results in the `output` folder of your current location. @@ -349,7 +347,19 @@ The URI of the web3 debug rpc provider, e.g. `file://$HOME/Library/Ethereum/geth #### `POSTGRES_URL` or `--postgres-url` [**Required**] -The PostgreSQL connection URL that the Hemera Indexer used to maintain its state. e.g. `postgresql+psycopg2://user:password@127.0.0.1:5432/postgres`. +The PostgreSQL connection URL that the Hemera Indexer used to maintain its state. e.g. `postgresql://user:password@127.0.0.1:5432/postgres`. + + +#### `ENTITY_TYPES` or `--entity-types` or `-E` + +[**Default**: `EXPLORER_BASE`] +The list of entity types to export. e.g. `EXPLORER_BASE`, `EXPLORER_TOKEN`, `EXPLORER_TRACE`. + +#### `OUTPUT_TYPES` or `--output-types` or `-O` + +The list of output types to export, corresponding to more detailed data models. Specifying this option will prioritize these settings over the entity types specified in -E. Available options include: Block, Transaction, Log, Token, AddressTokenBalance, etc. + +You may spawn up multiple Hemera Indexer processes, each of them specifying different output types to accelerate the indexing process. For example, indexing `trace` data may take much longer than other entities, you may want to run a separate process to index `trace` data. Checkout `docker-compose/docker-compose.yaml` for examples. #### `OUTPUT` or `--output` @@ -361,32 +371,11 @@ The file location will be relative to your current location if you run from sour e.g. -- `postgresql+psycopg2://user:password@localhost:5432/hemera_indexer`: Output will be exported to your postgres. +- `postgresql://user:password@localhost:5432/hemera_indexer`: Output will be exported to your postgres. - `jsonfile://output/json`: Json files will be exported to folder `output/json` - `csvfile://output/csv`: Csv files will be exported to folder `output/csv` - `console,jsonfile://output/json,csvfile://output/csv`: Multiple destinations are supported. -#### `ENTITY_TYPES` or `--entity-types` - -[**Default**: `BLOCK,TRANSACTION,LOG,TOKEN,TOKEN_TRANSFER`] -Hemera Indexer will export those entity types to your database and files(if `OUTPUT` is specified). -Full list of available entity types: - -- `block` -- `transaction` -- `log` -- `token` -- `token_transfer` -- `trace` -- `contract` -- `coin_balance` -- `token_balance` -- `token_ids` - -If you didn't specify this parameter, the default entity types will be BLOCK,TRANSACTION,LOG,TOKEN,TOKEN_TRANSFER. - -You may spawn up multiple Hemera Indexer processes, each of them indexing different entity types to accelerate the indexing process. For example, indexing `trace` data may take much longer than other entities, you may want to run a separate process to index `trace` data. Checkout `docker-compose/docker-compose.yaml` for examples. - #### `DB_VERSION` or `--db-version` [**Default**: `head`] diff --git a/indexer/utils/utils.py b/indexer/utils/utils.py index 34640d6e9..f866c07e0 100644 --- a/indexer/utils/utils.py +++ b/indexer/utils/utils.py @@ -174,7 +174,7 @@ def extract_eth_address(input_string): hex_string = hex_string.zfill(40) return Web3.to_checksum_address(hex_string).lower() - + def flatten(lst): result = [] From c7dc06875f23d1b6f331206e0abc9bd696dbf135 Mon Sep 17 00:00:00 2001 From: li xiang Date: Mon, 14 Oct 2024 20:03:22 +0800 Subject: [PATCH 12/12] Update ci.yaml (#187) --- .github/workflows/ci.yaml | 2 ++ .github/workflows/ut.yaml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2086e44dd..bcf07cff0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,6 +23,8 @@ jobs: ETHEREUM_PUBLIC_NODE_RPC_URL: '${{ secrets.ETHEREUM_PUBLIC_NODE_RPC_URL }}' LINEA_PUBLIC_NODE_DEBUG_RPC_URL: '${{ secrets.LINEA_PUBLIC_NODE_DEBUG_RPC_URL }}' LINEA_PUBLIC_NODE_RPC_URL: '${{ secrets.LINEA_PUBLIC_NODE_RPC_URL }}' + MANTLE_PUBLIC_NODE_RPC_URL: '${{ secrets.MANTLE_PUBLIC_NODE_RPC_URL }}' + MANTLE_PUBLIC_NODE_DEBUG_RPC_URL: '${{ secrets.MANTLE_PUBLIC_NODE_DEBUG_RPC_URL }}' POSTGRES_USER: hemera POSTGRES_PASSWORD: password POSTGRES_URL: postgresql://hemera:password@localhost:5432/hemera diff --git a/.github/workflows/ut.yaml b/.github/workflows/ut.yaml index a715be13c..196119358 100644 --- a/.github/workflows/ut.yaml +++ b/.github/workflows/ut.yaml @@ -33,6 +33,8 @@ jobs: ETHEREUM_PUBLIC_NODE_RPC_URL: '${{ secrets.ETHEREUM_PUBLIC_NODE_RPC_URL }}' LINEA_PUBLIC_NODE_DEBUG_RPC_URL: '${{ secrets.LINEA_PUBLIC_NODE_DEBUG_RPC_URL }}' LINEA_PUBLIC_NODE_RPC_URL: '${{ secrets.LINEA_PUBLIC_NODE_RPC_URL }}' + MANTLE_PUBLIC_NODE_RPC_URL: '${{ secrets.MANTLE_PUBLIC_NODE_RPC_URL }}' + MANTLE_PUBLIC_NODE_DEBUG_RPC_URL: '${{ secrets.MANTLE_PUBLIC_NODE_DEBUG_RPC_URL }}' run: | export PYTHONPATH=$(pwd) - make test indexer \ No newline at end of file + make test indexer