diff --git a/eth_portfolio/_db/entities.py b/eth_portfolio/_db/entities.py index 3dcd4bde..e04bdaff 100644 --- a/eth_portfolio/_db/entities.py +++ b/eth_portfolio/_db/entities.py @@ -54,85 +54,227 @@ class TokenExtended(Token, AddressExtended): class Transaction(DbEntity): + """ + Represents a transaction entity in the database. + + This class provides a property to access decoded transaction data, + which includes input data, signature components, and access lists. + The decoded data is accessed through the :attr:`decoded` property, + which returns a :class:`structs.Transaction` object. + + See Also: + - :class:`BlockExtended` + - :class:`AddressExtended` + - :class:`TokenTransfer` + """ + _id = PrimaryKey(int, auto=True) + "The primary key of the transaction." block = Required(BlockExtended, lazy=True, reverse="transactions") + "The block containing this transaction." transaction_index = Required(int, lazy=True) + "The index of the transaction within the block." hash = Required(str, index=True, lazy=True) + "The hash of the transaction." from_address = Required(AddressExtended, index=True, lazy=True, reverse="transactions_sent") + "The address that sent the transaction." to_address = Optional(AddressExtended, index=True, lazy=True, reverse="transactions_received") + "The address that received the transaction." value = Required(Decimal, 38, 18, lazy=True) + "The value transferred in the transaction." price = Optional(Decimal, 38, 18, lazy=True) + "The price of the transaction." value_usd = Optional(Decimal, 38, 18, lazy=True) + "The USD value of the transaction." nonce = Required(int, lazy=True) + "The nonce of the transaction." type = Optional(int, lazy=True) + "The type of the transaction." gas = Required(Decimal, 38, 1, lazy=True) + "The gas used by the transaction." gas_price = Required(Decimal, 38, 1, lazy=True) + "The gas price of the transaction." max_fee_per_gas = Optional(Decimal, 38, 1, lazy=True) + "The maximum fee per gas for the transaction." max_priority_fee_per_gas = Optional(Decimal, 38, 1, lazy=True) + "The maximum priority fee per gas for the transaction." composite_key(block, transaction_index) raw = Required(bytes, lazy=True) + "The raw bytes of the transaction." @cached_property def decoded(self) -> structs.Transaction: + """ + Decodes the raw transaction data into a :class:`structs.Transaction` object. + + Example: + >>> transaction = Transaction(...) + >>> decoded_transaction = transaction.decoded + >>> isinstance(decoded_transaction, structs.Transaction) + True + + See Also: + - :class:`structs.Transaction` + """ return json.decode(self.raw, type=structs.Transaction) @property def input(self) -> HexBytes: + """ + Returns the input data of the transaction. + + Example: + >>> transaction = Transaction(...) + >>> input_data = transaction.input + >>> isinstance(input_data, HexBytes) + True + + See Also: + - :attr:`structs.Transaction.input` + """ structs.Transaction.input.__doc__ return self.decoded.input @property def r(self) -> HexBytes: + """ + Returns the R component of the transaction's signature. + + Example: + >>> transaction = Transaction(...) + >>> r_value = transaction.r + >>> isinstance(r_value, HexBytes) + True + + See Also: + - :attr:`structs.Transaction.r` + """ structs.Transaction.r.__doc__ return self.decoded.r @property def s(self) -> HexBytes: + """ + Returns the S component of the transaction's signature. + + Example: + >>> transaction = Transaction(...) + >>> s_value = transaction.s + >>> isinstance(s_value, HexBytes) + True + + See Also: + - :attr:`structs.Transaction.s` + """ structs.Transaction.s.__doc__ return self.decoded.s @property def v(self) -> int: + """ + Returns the V component of the transaction's signature. + + Example: + >>> transaction = Transaction(...) + >>> v_value = transaction.v + >>> isinstance(v_value, int) + True + + See Also: + - :attr:`structs.Transaction.v` + """ structs.Transaction.v.__doc__ return self.decoded.v @property def access_list(self) -> typing.List[AccessListEntry]: + """ + Returns the access list of the transaction. + + Example: + >>> transaction = Transaction(...) + >>> access_list = transaction.access_list + >>> isinstance(access_list, list) + True + >>> isinstance(access_list[0], AccessListEntry) + True + + See Also: + - :class:`AccessListEntry` + - :attr:`structs.Transaction.access_list` + """ structs.Transaction.access_list.__doc__ return self.decoded.access_list @property def y_parity(self) -> typing.Optional[int]: - structs.TokenTransfer.y_parity.__doc__ + """ + Returns the y_parity of the transaction. + + Example: + >>> transaction = Transaction(...) + >>> y_parity_value = transaction.y_parity + >>> isinstance(y_parity_value, (int, type(None))) + True + + See Also: + - :attr:`structs.Transaction.y_parity` + """ + structs.Transaction.y_parity.__doc__ return self.decoded.y_parity class InternalTransfer(DbEntity): + """ + Represents an internal transfer entity in the database. + + This class provides properties to access decoded internal transfer data, + including input, output, and code. + + See Also: + - :class:`BlockExtended` + - :class:`AddressExtended` + """ + _id = PrimaryKey(int, auto=True) + "The primary key of the internal transfer." # common block = Required(BlockExtended, lazy=True, reverse="internal_transfers") + "The block containing this internal transfer." transaction_index = Required(int, lazy=True) + "The index of the transaction within the block." hash = Required(str, lazy=True) + "The hash of the internal transfer." from_address = Required( AddressExtended, index=True, lazy=True, reverse="internal_transfers_sent" ) + "The address that sent the internal transfer." to_address = Optional( AddressExtended, index=True, lazy=True, reverse="internal_transfers_received" ) + "The address that received the internal transfer." value = Required(Decimal, 38, 18, lazy=True) + "The value transferred in the internal transfer." price = Optional(Decimal, 38, 18, lazy=True) + "The price of the internal transfer." value_usd = Optional(Decimal, 38, 18, lazy=True) + "The USD value of the internal transfer." # unique type = Required(str, lazy=True) + "The type of the internal transfer." call_type = Required(str, lazy=True) + "The call type of the internal transfer." trace_address = Required(str, lazy=True) + "The trace address of the internal transfer." gas = Required(Decimal, 38, 1, lazy=True) + "The gas used by the internal transfer." gas_used = Optional(Decimal, 38, 1, lazy=True) + "The gas used by the internal transfer." composite_key( block, @@ -149,56 +291,152 @@ class InternalTransfer(DbEntity): ) raw = Required(bytes, lazy=True) + "The raw bytes of the internal transfer." @cached_property def decoded(self) -> structs.InternalTransfer: + """ + Decodes the raw internal transfer data into a :class:`structs.InternalTransfer` object. + + Example: + >>> internal_transfer = InternalTransfer(...) + >>> decoded_transfer = internal_transfer.decoded + >>> isinstance(decoded_transfer, structs.InternalTransfer) + True + + See Also: + - :class:`structs.InternalTransfer` + """ structs.InternalTransfer.__doc__ return json.decode(self.raw, type=structs.InternalTransfer) @property def code(self) -> HexBytes: + """ + Returns the code of the internal transfer. + + Example: + >>> internal_transfer = InternalTransfer(...) + >>> code_data = internal_transfer.code + >>> isinstance(code_data, HexBytes) + True + + See Also: + - :attr:`structs.InternalTransfer.code` + """ structs.InternalTransfer.code.__doc__ return self.decoded.code @property def input(self) -> HexBytes: + """ + Returns the input data of the internal transfer. + + Example: + >>> internal_transfer = InternalTransfer(...) + >>> input_data = internal_transfer.input + >>> isinstance(input_data, HexBytes) + True + + See Also: + - :attr:`structs.InternalTransfer.input` + """ structs.InternalTransfer.input.__doc__ return self.decoded.input @property def output(self) -> HexBytes: + """ + Returns the output data of the internal transfer. + + Example: + >>> internal_transfer = InternalTransfer(...) + >>> output_data = internal_transfer.output + >>> isinstance(output_data, HexBytes) + True + + See Also: + - :attr:`structs.InternalTransfer.output` + """ structs.InternalTransfer.output.__doc__ return self.decoded.output @property def subtraces(self) -> int: + """ + Returns the number of subtraces of the internal transfer. + + Example: + >>> internal_transfer = InternalTransfer(...) + >>> subtraces_count = internal_transfer.subtraces + >>> isinstance(subtraces_count, int) + True + + See Also: + - :attr:`structs.InternalTransfer.subtraces` + """ structs.InternalTransfer.subtraces.__doc__ return self.decoded.subtraces class TokenTransfer(DbEntity): + """ + Represents a token transfer entity in the database. + + This class provides properties to access decoded token transfer data. + + See Also: + - :class:`BlockExtended` + - :class:`AddressExtended` + - :class:`TokenExtended` + """ + _id = PrimaryKey(int, auto=True) + "The primary key of the token transfer." # common block = Required(BlockExtended, lazy=True, reverse="token_transfers") + "The block containing this token transfer." transaction_index = Required(int, lazy=True) + "The index of the transaction within the block." hash = Required(str, lazy=True) + "The hash of the token transfer." from_address = Required(AddressExtended, index=True, lazy=True, reverse="token_transfers_sent") + "The address that sent the token transfer." to_address = Required( AddressExtended, index=True, lazy=True, reverse="token_transfers_received" ) + "The address that received the token transfer." value = Required(Decimal, 38, 18, lazy=True) + "The value transferred in the token transfer." price = Optional(Decimal, 38, 18, lazy=True) + "The price of the token transfer." value_usd = Optional(Decimal, 38, 18, lazy=True) + "The USD value of the token transfer." # unique log_index = Required(int, lazy=True) + "The log index of the token transfer." token = Optional(TokenExtended, index=True, lazy=True, reverse="transfers") + "The token involved in the transfer." composite_key(block, transaction_index, log_index) raw = Required(bytes, lazy=True) + "The raw bytes of the token transfer." @cached_property def decoded(self) -> structs.TokenTransfer: + """ + Decodes the raw token transfer data into a :class:`structs.TokenTransfer` object. + + Example: + >>> token_transfer = TokenTransfer(...) + >>> decoded_transfer = token_transfer.decoded + >>> isinstance(decoded_transfer, structs.TokenTransfer) + True + + See Also: + - :class:`structs.TokenTransfer` + """ return json.decode(self.raw, type=structs.TokenTransfer) diff --git a/eth_portfolio/_decimal.py b/eth_portfolio/_decimal.py index e8b3f083..42019912 100644 --- a/eth_portfolio/_decimal.py +++ b/eth_portfolio/_decimal.py @@ -61,39 +61,109 @@ def jsonify(self) -> Union[str, int]: while string[-1] == "0": string = string[:-1] - if type(self)(scientific_notation) == self and len(scientific_notation) < len(string): + if len(scientific_notation) < len(string): return scientific_notation return string def __add__(self, other): + """ + Adds two :class:`Decimal` values, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('1.1') + Decimal('2.2') + Decimal('3.3') + """ return type(self)(super().__add__(other)) def __radd__(self, other): + """ + Adds two :class:`Decimal` values with reflected operands, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('1.1').__radd__(Decimal('2.2')) + Decimal('3.3') + """ return type(self)(super().__radd__(other)) def __sub__(self, other): + """ + Subtracts two :class:`Decimal` values, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('3.3') - Decimal('1.1') + Decimal('2.2') + """ return type(self)(super().__sub__(other)) def __rsub__(self, other): + """ + Subtracts two :class:`Decimal` values with reflected operands, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('3.3').__rsub__(Decimal('1.1')) + Decimal('-2.2') + """ return type(self)(super().__rsub__(other)) def __mul__(self, other): + """ + Multiplies two :class:`Decimal` values, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('2') * Decimal('3') + Decimal('6') + """ return type(self)(super().__mul__(other)) def __rmul__(self, other): + """ + Multiplies two :class:`Decimal` values with reflected operands, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('2').__rmul__(Decimal('3')) + Decimal('6') + """ return type(self)(super().__rmul__(other)) def __truediv__(self, other): + """ + Divides two :class:`Decimal` values, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('6') / Decimal('3') + Decimal('2') + """ return type(self)(super().__truediv__(other)) def __rtruediv__(self, other): + """ + Divides two :class:`Decimal` values with reflected operands, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('6').__rtruediv__(Decimal('3')) + Decimal('0.5') + """ return type(self)(super().__rtruediv__(other)) def __floordiv__(self, other): + """ + Performs floor division on two :class:`Decimal` values, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('7') // Decimal('3') + Decimal('2') + """ return type(self)(super().__floordiv__(other)) def __rfloordiv__(self, other): + """ + Performs floor division on two :class:`Decimal` values with reflected operands, ensuring the result is of type :class:`Decimal`. + + Examples: + >>> Decimal('7').__rfloordiv__(Decimal('3')) + Decimal('0') + """ return type(self)(super().__rfloordiv__(other)) diff --git a/eth_portfolio/_exceptions.py b/eth_portfolio/_exceptions.py index 07847ee8..e8285263 100644 --- a/eth_portfolio/_exceptions.py +++ b/eth_portfolio/_exceptions.py @@ -11,7 +11,7 @@ class BlockRangeIsCached(Exception): """ Exception raised when a block range is already cached. - This exception is used to indicate that the requested block range + This exception indicates that the requested block range has already been loaded into memory and does not need to be fetched again. Examples: @@ -25,7 +25,7 @@ class BlockRangeOutOfBounds(Exception): """ Exception raised when a block range is out of bounds. - This exception is used to indicate that the requested block range + This exception indicates that the requested block range is outside the bounds of the cached data. It provides a method to handle the loading of the remaining ledger entries that are out of bounds by invoking the appropriate method in the associated ledger. diff --git a/eth_portfolio/_ydb/token_transfers.py b/eth_portfolio/_ydb/token_transfers.py index 3d129c66..83ac0b5a 100644 --- a/eth_portfolio/_ydb/token_transfers.py +++ b/eth_portfolio/_ydb/token_transfers.py @@ -34,11 +34,32 @@ class _TokenTransfers(ProcessedEvents["Task[TokenTransfer]"]): - """A helper mixin that contains all logic for fetching token transfers for a particular wallet address""" + """ + A helper mixin that contains all logic for fetching token transfers for a particular address. + + Examples: + Fetching token transfers for a specific address: + + >>> transfers = _TokenTransfers(address="0x123...", from_block=0, load_prices=True) + >>> async for transfer in transfers.yield_thru_block(1000000): + ... print(transfer) + + See Also: + - :class:`~eth_portfolio.structs.TokenTransfer`: For the structure of a token transfer. + - :func:`~eth_portfolio._loaders.load_token_transfer`: For loading token transfer data. + """ __slots__ = "address", "_load_prices" def __init__(self, address: Address, from_block: int, load_prices: bool = False): + """ + Initialize a _TokenTransfers instance. + + Args: + address: The address for which token transfers are fetched. + from_block: The block number from which to start fetching token transfers. + load_prices: Indicates whether to load prices for the token transfers. + """ self.address = address self._load_prices = load_prices super().__init__(topics=self._topics, from_block=from_block) @@ -48,10 +69,24 @@ def __repr__(self) -> str: @property @abstractmethod - def _topics(self) -> List: ... + def _topics(self) -> List: + pass @ASyncIterator.wrap # type: ignore [call-overload] async def yield_thru_block(self, block) -> AsyncIterator["Task[TokenTransfer]"]: + """ + Yield token transfers up to a specified block. + + Args: + block: The block number up to which token transfers are yielded. + + Yields: + Tasks that resolve to :class:`~eth_portfolio.structs.TokenTransfer` objects. + + Examples: + >>> async for transfer in transfers.yield_thru_block(1000000): + ... print(transfer) + """ if not _logger_is_enabled_for(DEBUG): async for task in self._objects_thru(block=block): yield task @@ -68,6 +103,15 @@ async def yield_thru_block(self, block) -> AsyncIterator["Task[TokenTransfer]"]: _logger_log(DEBUG, "%s yield thru %s complete", (self, block)) async def _extend(self, objs: List[evmspec.Log]) -> None: + """ + Extend the list of token transfers with new logs. + + Args: + objs: A list of :class:`~evmspec.Log` objects representing token transfer logs. + + Examples: + >>> await transfers._extend(logs) + """ shitcoins = SHITCOINS.get(chain.id, set()) append_loader_task = self._objects.append done = 0 @@ -98,7 +142,19 @@ def _done_callback(self, task: Task) -> None: class InboundTokenTransfers(_TokenTransfers): - """A container that fetches and iterates over all inbound token transfers for a particular wallet address""" + """ + A container that fetches and iterates over all inbound token transfers for a particular address. + + Examples: + Fetching inbound token transfers for a specific address: + + >>> inbound_transfers = InboundTokenTransfers(address="0x123...", from_block=0, load_prices=True) + >>> async for transfer in inbound_transfers.yield_thru_block(1000000): + ... print(transfer) + + See Also: + - :class:`~eth_portfolio.structs.TokenTransfer`: For the structure of a token transfer. + """ @property def _topics(self) -> List: @@ -106,7 +162,19 @@ def _topics(self) -> List: class OutboundTokenTransfers(_TokenTransfers): - """A container that fetches and iterates over all outbound token transfers for a particular wallet address""" + """ + A container that fetches and iterates over all outbound token transfers for a particular address. + + Examples: + Fetching outbound token transfers for a specific address: + + >>> outbound_transfers = OutboundTokenTransfers(address="0x123...", from_block=0, load_prices=True) + >>> async for transfer in outbound_transfers.yield_thru_block(1000000): + ... print(transfer) + + See Also: + - :class:`~eth_portfolio.structs.TokenTransfer`: For the structure of a token transfer. + """ @property def _topics(self) -> List: @@ -115,8 +183,19 @@ def _topics(self) -> List: class TokenTransfers(ASyncIterable[TokenTransfer]): """ - A container that fetches and iterates over all token transfers for a particular wallet address. - NOTE: These do not come back in chronologcal order. + A container that fetches and iterates over all token transfers for a particular address. + + Examples: + Fetching all token transfers for a specific address: + + >>> token_transfers = TokenTransfers(address="0x123...", from_block=0, load_prices=True) + >>> async for transfer in token_transfers: + ... print(transfer) + + See Also: + - :class:`~eth_portfolio.structs.TokenTransfer`: For the structure of a token transfer. + - :class:`~InboundTokenTransfers`: For fetching inbound token transfers. + - :class:`~OutboundTokenTransfers`: For fetching outbound token transfers. """ def __init__(self, address: Address, from_block: int, load_prices: bool = False): @@ -124,10 +203,33 @@ def __init__(self, address: Address, from_block: int, load_prices: bool = False) self.transfers_out = OutboundTokenTransfers(address, from_block, load_prices=load_prices) async def __aiter__(self): + """ + Asynchronously iterate over all token transfers. + + Yields: + :class:`~eth_portfolio.structs.TokenTransfer` objects. + + Examples: + >>> async for transfer in token_transfers: + ... print(transfer) + """ async for transfer in self.yield_thru_block(await dank_mids.eth.block_number): yield transfer def yield_thru_block(self, block: int) -> ASyncIterator["Task[TokenTransfer]"]: + """ + Yield token transfers up to a specified block. + + Args: + block: The block number up to which token transfers are yielded. + + Yields: + Tasks that resolve to :class:`~eth_portfolio.structs.TokenTransfer` objects. + + Examples: + >>> async for transfer in token_transfers.yield_thru_block(1000000): + ... print(transfer) + """ return ASyncIterator( as_yielded( self.transfers_in.yield_thru_block(block), diff --git a/eth_portfolio/address.py b/eth_portfolio/address.py index 9ae3eac4..8f7de88c 100644 --- a/eth_portfolio/address.py +++ b/eth_portfolio/address.py @@ -48,6 +48,19 @@ class PortfolioAddress(_LedgeredBase[AddressLedgerBase]): """ Represents a portfolio address within the eth-portfolio system. + + This class is designed to manage different aspects of an Ethereum address within the portfolio, + such as transactions, transfers, balances, and interactions with both external and lending protocols. + + Key components and functionalities provided by the :class:`~eth_portfolio.address.PortfolioAddress` class include: + - Handling Ethereum and token balances + - Managing debt and collateral from lending protocols + - Tracking transactions and transfers (both internal and token transfers) + - Providing comprehensive balance descriptions at specific block heights + + The class leverages asynchronous operations using the `a_sync` library to efficiently gather and process data. + It also integrates with various submodules from `eth-portfolio` to load balances, manage ledgers, and interact + with external protocols. """ def __init__( @@ -59,19 +72,30 @@ def __init__( asynchronous: bool = False, ) -> None: # type: ignore """ - Initializes the PortfolioAddress instance. + Initializes the :class:`~PortfolioAddress` instance. Args: - address: The address to manage. - portfolio: The portfolio instance managing this address. + address: The Ethereum address to manage. + start_block: The block number from which to start tracking. + load_prices: Flag indicating if price loading is enabled. + num_workers_transactions (optional): Number of workers for transaction processing. Defaults to 1000. asynchronous (optional): Flag for asynchronous operation. Defaults to False. Raises: TypeError: If `asynchronous` is not a boolean. Examples: - >>> portfolio = Portfolio() - >>> address = PortfolioAddress('0x1234...', portfolio) + >>> address = PortfolioAddress('0x1234...', 0, True) + >>> print(address) + + >>> address = PortfolioAddress('0x1234...', 0, False, num_workers_transactions=500, asynchronous=True) + >>> print(address) + + See Also: + - :class:`~eth_portfolio.portfolio.Portfolio` + - :class:`~eth_portfolio._ledgers.address.AddressTransactionsLedger` + - :class:`~eth_portfolio._ledgers.address.AddressInternalTransfersLedger` + - :class:`~eth_portfolio._ledgers.address.AddressTokenTransfersLedger` """ self.address = convert.to_address(address) """ diff --git a/eth_portfolio/buckets.py b/eth_portfolio/buckets.py index 8208f0ad..06222595 100644 --- a/eth_portfolio/buckets.py +++ b/eth_portfolio/buckets.py @@ -25,8 +25,8 @@ async def get_token_bucket(token: AnyAddressType) -> str: This function attempts to categorize a given token into predefined buckets such as "Cash & cash equivalents", "ETH", "BTC", "Other long term assets", or "Other short term assets". The categorization is based on the token's - characteristics and its presence in specific sets like `ETH_LIKE`, `BTC_LIKE`, - and `OTHER_LONG_TERM_ASSETS`. + characteristics and its presence in specific sets like :data:`ETH_LIKE`, :data:`BTC_LIKE`, + and :data:`OTHER_LONG_TERM_ASSETS`. Args: token: The address of the token to categorize. @@ -39,9 +39,16 @@ async def get_token_bucket(token: AnyAddressType) -> str: does not match the expected pattern. Example: + Categorize a stablecoin: + >>> await get_token_bucket("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") 'Cash & cash equivalents' + Categorize an ETH-like token: + + >>> await get_token_bucket("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") + 'ETH' + See Also: - :func:`_unwrap_token` - :func:`_is_stable` @@ -82,6 +89,8 @@ async def _unwrap_token(token) -> str: The address of the underlying asset. Example: + Unwrap a Yearn vault token: + >>> await _unwrap_token("0x5f18C75AbDAe578b483E5F43f12a39cF75b973a9") '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' @@ -128,6 +137,8 @@ def _pool_bucket(pool_tokens: set) -> Optional[str]: no specific category is found. Example: + Determine the bucket for a BTC-like pool: + >>> _pool_bucket({"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"}) '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' @@ -151,13 +162,15 @@ def _is_stable(token: Address) -> bool: """ Check if a token is a stablecoin. - This function checks if a given token is present in the `STABLECOINS` or - `INTL_STABLECOINS` sets, indicating that it is a stablecoin. + This function checks if a given token is present in the :data:`STABLECOINS` or + :data:`INTL_STABLECOINS` sets, indicating that it is a stablecoin. Args: token: The address of the token to check. Example: + Check if a token is a stablecoin: + >>> _is_stable("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") True diff --git a/eth_portfolio/protocols/lending/liquity.py b/eth_portfolio/protocols/lending/liquity.py index 24339c93..ef754222 100644 --- a/eth_portfolio/protocols/lending/liquity.py +++ b/eth_portfolio/protocols/lending/liquity.py @@ -29,6 +29,7 @@ class Liquity(LendingProtocolWithLockedCollateral): """ networks = [Network.Mainnet] + """The networks on which the protocol is available.""" def __init__(self) -> None: self.troveManager = Contract("0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2") @@ -54,6 +55,20 @@ async def get_trove(self, address: Address, block: Block) -> dict: @stuck_coro_debugger async def _balances(self, address: Address, block: Optional[Block] = None) -> TokenBalances: + """ + Retrieves the collateral balances for a given address at a specific block. + + Args: + address (Address): The Ethereum address of the user. + block (Optional[Block]): The block number to query. + + Examples: + >>> balances = await liquity._balances("0xYourAddress", 12345678) + >>> print(balances) + + See Also: + - :class:`~eth_portfolio.typing.TokenBalances` + """ balances: TokenBalances = TokenBalances(block=block) if block and block < self.start_block: return balances diff --git a/setup.py b/setup.py index 9b30e465..74f905ed 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,5 @@ from setuptools import find_packages, setup # type: ignore -with open("requirements.txt", "r") as f: - requirements = list(map(str.strip, f.read().split("\n")))[:-1] - -setup( - name="eth-portfolio", - packages=find_packages(), - use_scm_version={ - "root": ".", - "relative_to": __file__, - "local_scheme": "no-local-version", - "version_scheme": "python-simplified-semver", - }, - description="eth-portfolio makes it easy to analyze your portfolio.", - author="BobTheBuidler", - author_email="bobthebuidlerdefi@gmail.com", - url="https://github.com/BobTheBuidler/eth-portfolio", - install_requires=requirements, - setup_requires=["setuptools_scm", "cython"], - package_data={ - "eth_portfolio": ["py.typed"], - }, -) - """ Installation ------------ @@ -30,13 +7,17 @@ Due to the use of :mod:`setuptools_scm` for versioning, it is recommended to clone the repository first to ensure the version can be determined correctly. -The `setup.py` file automatically handles the installation of :mod:`setuptools_scm` and :mod:`cython`, -so you do not need to install them manually before running the setup process. Additionally, -the `requirements.txt` file is used to specify additional dependencies that are installed via -the `install_requires` parameter. Note that the last line of `requirements.txt` is intentionally excluded -from installation, so ensure that any necessary dependency is not placed on the last line. +The `setup.py` file specifies the installation of :mod:`setuptools_scm` and :mod:`cython` +via the `setup_requires` parameter. These dependencies must be available in your environment +before running the setup process. Additionally, the `requirements.txt` file is used to specify +additional dependencies that are installed via the `install_requires` parameter. Note that the +last line of `requirements.txt` is typically an empty string due to the split operation and is +therefore excluded from installation. Ensure that any necessary dependency is not placed on the +last line before the empty line. Example: + Clone the repository and install the package: + .. code-block:: bash git clone https://github.com/BobTheBuidler/eth-portfolio.git @@ -46,6 +27,8 @@ If you encounter issues with :mod:`PyYaml` and :mod:`Cython`, you can resolve them by installing specific versions: Example: + Install specific versions of dependencies to resolve issues: + .. code-block:: bash pip install wheel @@ -57,3 +40,26 @@ - :mod:`cython`: For more information on Cython. - :mod:`requirements.txt`: For more information on managing dependencies. """ + +with open("requirements.txt", "r") as f: + requirements = list(map(str.strip, f.read().split("\n")))[:-1] + +setup( + name="eth-portfolio", + packages=find_packages(), + use_scm_version={ + "root": ".", + "relative_to": __file__, + "local_scheme": "no-local-version", + "version_scheme": "python-simplified-semver", + }, + description="eth-portfolio makes it easy to analyze your portfolio.", + author="BobTheBuidler", + author_email="bobthebuidlerdefi@gmail.com", + url="https://github.com/BobTheBuidler/eth-portfolio", + install_requires=requirements, + setup_requires=["setuptools_scm", "cython"], + package_data={ + "eth_portfolio": ["py.typed"], + }, +) diff --git a/tests/conftest.py b/tests/conftest.py index 093aeec1..8bebf9e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,25 +1,7 @@ -import os -import sys - -from brownie import network - -sys.path.insert(0, os.path.abspath(".")) - -try: - brownie_network = os.environ["PYTEST_NETWORK"] -except KeyError: - raise ValueError( - "Please set the PYTEST_NETWORK environment variable to the name of the brownie network you want to use for testing." - ) - -if not network.is_connected(): - network.connect(brownie_network) - """ -This module configures the testing environment for the project by ensuring -that the Brownie network is connected using the specified network. It also -modifies the system path to include the current directory, allowing for -importing modules from the project root. +This module connects to the Brownie network specified by the `PYTEST_NETWORK` +environment variable. It also modifies the system path to include the current +directory, allowing for importing modules from the project root. Environment Variables: PYTEST_NETWORK: The name of the Brownie network to use for testing. This @@ -36,10 +18,27 @@ export PYTEST_NETWORK=mainnet-fork pytest - This will connect to the specified Brownie network and run the tests. + This will attempt to connect to the specified Brownie network and run the tests. See Also: - :mod:`brownie.network`: For more information on managing network connections with Brownie. - :mod:`os`: For more information on interacting with the operating system. - :mod:`sys`: For more information on the system-specific parameters and functions. """ + +import os +import sys + +from brownie import network + +sys.path.insert(0, os.path.abspath(".")) + +try: + brownie_network = os.environ["PYTEST_NETWORK"] +except KeyError: + raise ValueError( + "Please set the PYTEST_NETWORK environment variable to the name of the brownie network you want to use for testing." + ) + +if not network.is_connected(): + network.connect(brownie_network) diff --git a/tests/protocols/test_external.py b/tests/protocols/test_external.py index 32c015fe..212e69cf 100644 --- a/tests/protocols/test_external.py +++ b/tests/protocols/test_external.py @@ -21,7 +21,8 @@ class MockProtocolB(AsyncMock): @patch("a_sync.map") @pytest.mark.asyncio async def test_balances_no_protocols(mock_map): - """Test the `balances` function with no protocols. + """ + Test the `balances` function with no protocols. This test verifies that when there are no protocols in the `protocols.protocols` list, the `balances` function returns an @@ -44,7 +45,8 @@ async def test_balances_no_protocols(mock_map): @pytest.mark.asyncio async def test_balances_with_protocols(): - """Test the `balances` function with multiple protocols. + """ + Test the `balances` function with multiple protocols. This test verifies that when there are multiple protocols in the `protocols.protocols` list, the `balances` function correctly @@ -80,7 +82,8 @@ async def test_balances_with_protocols(): @pytest.mark.asyncio async def test_balances_with_protocols_and_block(): - """Test the `balances` function with protocols and a specific block. + """ + Test the `balances` function with protocols and a specific block. This test verifies that when there are multiple protocols in the `protocols.protocols` list and a specific block is provided, the