Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): a bunch of docs #693

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,49 @@
mainnet_only = pytest.mark.skipif(
chain.id != Network.Mainnet, reason="This test is only applicable on mainnet"
)
"""
A pytest marker to skip tests that are only applicable on the Ethereum mainnet.
"""

optimism_only = pytest.mark.skipif(
chain.id != Network.Optimism, reason="This test is only applicable on optimism"
)
"""
A pytest marker to skip tests that are only applicable on the Optimism network.
"""

async_test = pytest.mark.asyncio_cooperative
"""
A pytest marker for concurrent asynchronous tests using asyncio_cooperative.
"""

def blocks_for_contract(address: Address, count: int = 5) -> List[Block]:
"""
Generate a list of block numbers for testing a contract.

Args:
address: The address of the contract.
count: The number of blocks to generate (default is 5).

Returns:
A list of block numbers evenly spaced between the contract's creation and the current block height.
"""
address = convert.to_address(address)
return [int(block) for block in np.linspace(contract_creation_block(address) + 10000, chain.height, count)]

def mutate_address(address: Address) -> Tuple[str,str,str,EthAddress]:
"""
Returns the same address in various forms for testing.

Args:
address: The original address.

Returns:
A tuple containing the address in four different forms:
1. Lowercase string
2. Checksum address string
3. Address object
4. EthAddress object
"""
return (
address.lower(),
Expand Down
21 changes: 20 additions & 1 deletion y/ENVIRONMENT_VARIABLES.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,39 @@

_envs = EnvVarFactory("YPRICEMAGIC")

# TTL for various in-memory caches thruout the library
CACHE_TTL = _envs.create_env("CACHE_TTL", int, default=60*60, verbose=False)
"""TTL for various in-memory caches throughout the library"""

CONTRACT_CACHE_TTL = _envs.create_env("CONTRACT_CACHE_TTL", int, default=int(CACHE_TTL), verbose=False)
"""TTL for contract cache, defaults to :obj:`CACHE_TTL` if not set"""

GETLOGS_BATCH_SIZE = _envs.create_env("GETLOGS_BATCH_SIZE", int, default=0)
"""Batch size for getlogs operations, 0 will use default as determined by your provider."""

GETLOGS_DOP = _envs.create_env("GETLOGS_DOP", int, default=32)
"""Degree of parallelism for eth_getLogs operations"""

DB_PROVIDER = _envs.create_env("DB_PROVIDER", str, default="sqlite", verbose=False)
"""Database provider (e.g., 'sqlite', 'postgresql')"""

DB_HOST = _envs.create_env("DB_HOST", str, default="", verbose=False)
"""Database host address"""

DB_PORT = _envs.create_env("DB_PORT", str, default="", verbose=False)
"""Database port number"""

DB_USER = _envs.create_env("DB_USER", str, default="", verbose=False)
"""Database user name"""

DB_PASSWORD = _envs.create_env("DB_PASSWORD", str, default="", verbose=False)
"""Database password"""

DB_DATABASE = _envs.create_env("DB_DATABASE", str, default="ypricemagic", verbose=False)
"""Database name"""

SKIP_CACHE = _envs.create_env("SKIP_CACHE", bool, default=False, verbose=False)
"""Flag to skip cache usage"""

# ypriceapi
SKIP_YPRICEAPI = create_env("SKIP_YPRICEAPI", bool, default=False, verbose=False)
"""Flag to skip using ypriceapi"""
85 changes: 83 additions & 2 deletions y/_db/utils/bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,33 @@
logger = logging.getLogger(__name__)

class SQLError(ValueError):
...
"""
Custom exception for SQL-related errors.

This exception wraps an Exception raised when executing a SQL statement.
It includes both the original error and the SQL statement that caused it.
"""

@retry_locked
def execute(sql: str, *, db: Database = entities.db) -> None:
"""
Execute a SQL statement with retry logic for locked databases.

This function attempts to execute the given SQL statement and commit the changes.
If the database is locked, the operation will be retried based on the @:class:`retry_locked` decorator.

Args:
sql: The SQL statement to execute.
db: The database to execute the statement on. Defaults to entities.db.

Raises:
SQLError: If there's an error executing the SQL statement, except for "database is locked" errors.

Note:
- The function logs the SQL statement at debug level before execution.
- If a "database is locked" error occurs, it's re-raised to trigger the retry mechanism.
- For all other DatabaseErrors, it logs a warning and raises a SQLError with the original error and SQL statement.
"""
try:
logger.debug("EXECUTING SQL")
logger.debug(sql)
Expand All @@ -28,6 +51,29 @@ def execute(sql: str, *, db: Database = entities.db) -> None:
raise SQLError(e, sql) from e

def stringify_column_value(value: Any, provider: str) -> str:
"""
Convert a Python value to a string representation suitable for SQL insertion.

This function handles various Python types and converts them to SQL-compatible string representations.
It supports different database providers (currently 'postgres' and 'sqlite') for certain data types.

Args:
value: The value to stringify. Can be None, bytes, str, int, Decimal, or datetime.
provider: The database provider. Currently supports 'postgres' and 'sqlite'.

Returns:
A string representation of the value suitable for SQL insertion.

Raises:
NotImplementedError: If the value type is not supported or if an unsupported provider is specified for bytes.

Note:
- None values are converted to 'null'.
- Bytes are handled differently for 'postgres' (converted to bytea) and 'sqlite' (converted to hex).
- Strings are wrapped in single quotes.
- Integers and Decimals are converted to their string representation.
- Datetimes are converted to UTC and formatted as ISO8601 strings.
"""
if value is None:
return 'null'
elif isinstance(value, bytes):
Expand All @@ -45,8 +91,24 @@ def stringify_column_value(value: Any, provider: str) -> str:
return f"'{value.astimezone(timezone.utc).isoformat()}'"
else:
raise NotImplementedError(type(value), value)

def build_row(row: Iterable[Any], provider: str) -> str:
"""
Build a SQL row string from an iterable of values.

This function takes an iterable of values and converts each value to its SQL string representation,
then combines them into a single row string suitable for SQL insertion.

Args:
row: An iterable of values to be converted into a SQL row string.
provider: The database provider to use for value conversion.

Returns:
A string representing a SQL row, formatted as (value1,value2,...).

Note:
This function uses :func:`stringify_column_value` internally to convert each value.
"""
return f"({','.join(stringify_column_value(col, provider) for col in row)})"

@a_sync_write_db_session
Expand All @@ -57,6 +119,25 @@ def insert(
*,
db: Database = entities.db,
) -> None:
"""
Perform a bulk insert operation into the database.

This function constructs and executes an INSERT statement for multiple rows of data.
It supports different syntax for SQLite and PostgreSQL databases.

Args:
entity_type: The database entity type to insert into.
columns: An iterable of column names.
items: An iterable of iterables, where each inner iterable represents a row of data to insert.
db: The database to perform the insertion on. Defaults to entities.db.

Raises:
NotImplementedError: If an unsupported database provider is used.

Note:
- For SQLite, it uses 'INSERT OR IGNORE' syntax.
- For PostgreSQL, it uses 'INSERT ... ON CONFLICT DO NOTHING' syntax.
"""
entity_name = entity_type.__name__.lower()
data = ",".join(build_row(i, db.provider_name) for i in items)
if db.provider_name == 'sqlite':
Expand Down
50 changes: 47 additions & 3 deletions y/classes/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,43 @@


class Wrapper(ERC20):
"""An ERC20 token that holds balances of (an) other ERC20 token(s)"""
"""
An abstract base class representing an ERC20 token that holds balances of (an)other ERC20 token(s).

This class extends the ERC20 class to represent wrapper tokens, which are tokens that derive their value
from holding other tokens. The specific implementation details are left to the subclasses.

Note:
This class is currently a placeholder and does not implement any additional methods.
"""
# TODO: implement


class LiquidityPool(Wrapper):
"""A :ref:`~Wrapper` that pools multiple ERC20s together for swapping"""
"""
An abstract base class representing a liquidity pool that pools multiple ERC20s together for swapping.

This class extends the :class:`~Wrapper` class to represent liquidity pools, which are special types of wrapper
tokens that allow for token swaps. It provides methods for getting the total value locked (TVL) and pool price.
"""

# TODO: implement this elsewhere outside of just balancer

@stuck_coro_debugger
async def get_pool_price(self, block: Optional[Block] = None, skip_cache: bool = ENVS.SKIP_CACHE) -> UsdPrice:
"""
Calculate the price of the liquidity pool token.

This method calculates the price of a single liquidity pool token by dividing the total value locked (TVL)
by the total supply of the pool tokens.

Args:
block: The block number at which to calculate the price. If None, uses the latest block.
skip_cache: If True, bypasses ypricemagic's local caching mechanisms and forces a fresh calculation.

Returns:
The price of a single liquidity pool token as a UsdPrice object.
"""
tvl, total_supply = await asyncio.gather(
self.get_tvl(block=block, skip_cache=skip_cache, sync=False),
self.total_supply_readable(block=block, sync=False),
Expand All @@ -27,4 +56,19 @@ async def get_pool_price(self, block: Optional[Block] = None, skip_cache: bool =

@abc.abstractmethod
async def get_tvl(self, block: Optional[Block] = None, skip_cache: bool = ENVS.SKIP_CACHE) -> UsdValue:
...
"""
Get the Total Value Locked (TVL) in the liquidity pool.

This is an abstract method that must be implemented by subclasses. It should return the total value
of all assets locked in the liquidity pool.

Args:
block: The block number at which to calculate the TVL. If None, uses the latest block.
skip_cache: If True, bypasses ypricemagic's local caching mechanisms and forces a fresh calculation.

Returns:
The Total Value Locked (TVL) in the pool as a UsdValue object.

Note:
The specific implementation of this method will depend on the type of liquidity pool.
"""
41 changes: 33 additions & 8 deletions y/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,45 @@
from y.prices.dex.uniswap.v2 import UniswapV2Pool
from y.prices.stable_swap.curve import CurvePool

Pool = Union["UniswapV2Pool", "CurvePool"]

Address = Union[str,HexBytes,AnyAddress,EthAddress]
Block = Union[int,BlockNumber]
Address = Union[str, HexBytes, AnyAddress, EthAddress]
"""
A union of types used to represent Ethereum addresses.
"""

Block = Union[int, BlockNumber]
"""
A union of types used to represent block numbers as integers.
"""

AddressOrContract = Union[Address,Contract]
AnyAddressType = Union[Address,Contract,int]

AnyAddressType = Union[Address, Contract, int]
"""
A type alias representing any valid representation of an Ethereum address.
This can be an Address, a :class:`~y.Contract`, or an integer.
"""

Pool = Union["UniswapV2Pool", "CurvePool"]
"""
A union of types representing liquidity pools.
"""

class UsdValue(float):
def __init__(self, v) -> None:
super().__init__()
"""
Represents a USD value with custom string representation.
"""

def __str__(self) -> str:
"""
Return a string representation of the USD value.

Returns:
A string formatted as a USD value with 8 decimal places.
"""
return f'${self:.8f}'

class UsdPrice(UsdValue):
def __init__(self, v) -> None:
super().__init__(v)
"""
Represents a USD price.
"""
Loading
Loading