diff --git a/pyproject.toml b/pyproject.toml index 75672167..ad8d3f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,16 +11,17 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<4" -eth-ape = "^0.6.21" -pydantic = "^1.10.13" -silverback = ">=0.1.0,<0.3" +python = ">=3.8.1,<4" +eth-ape = "^0.7.3" +silverback = "^0.3" [tool.poetry.group.test.dependencies] -ape-foundry = "^0.6.16" +ape-foundry = "^0.7.1" [tool.poetry.group.dev.dependencies] black = "^23.3.0" +isort = "^5.13.2" +mypy = "^1.8.0" [build-system] requires = ["poetry-core"] diff --git a/sdk/py/apepay/__init__.py b/sdk/py/apepay/__init__.py index 6d3a29ba..0bcab295 100644 --- a/sdk/py/apepay/__init__.py +++ b/sdk/py/apepay/__init__.py @@ -1,23 +1,25 @@ -import json import importlib +import json from datetime import datetime, timedelta from decimal import Decimal from functools import partial -from typing import Any, Dict, Iterable, Iterator, List, Optional, Union, cast, ClassVar +from pathlib import Path +from typing import Any, ClassVar, Dict, Iterable, Iterator, List, Optional, Union, cast from ape.api import ReceiptAPI from ape.contracts.base import ContractInstance, ContractTransactionHandler from ape.exceptions import ( CompilerError, ContractLogicError, + ContractNotFoundError, DecodingError, ProjectError, - ContractNotFoundError, ) -from ape.types import AddressType, ContractLog, HexBytes +from ape.types import AddressType, ContractLog from ape.utils import BaseInterfaceModel, cached_property from ethpm_types import ContractType, PackageManifest -from pydantic import ValidationError, validator +from hexbytes import HexBytes +from pydantic import ValidationError, ValidationInfo, field_validator from .exceptions import ( FundsNotClaimable, @@ -48,7 +50,7 @@ def __eq__(self, other: Any) -> bool: # Try __eq__ from the other side. return NotImplemented - def validate(self, creator, token, amount_per_second, reason) -> bool: + def validate(self, creator, token, amount_per_second, reason) -> bool: # type: ignore try: self.contract.validate.call(creator, token, amount_per_second, reason) return True @@ -63,14 +65,16 @@ def validate(self, creator, token, amount_per_second, reason) -> bool: class StreamManager(BaseInterfaceModel): address: AddressType contract_type: Optional[ContractType] = None - _local_contracts: ClassVar[Dict[str, ContractType]] + _local_contracts: ClassVar[Dict[str, ContractType]] = dict() - @validator("address", pre=True) + @field_validator("address", mode="before") + @classmethod def normalize_address(cls, value: Any) -> AddressType: return cls.conversion_manager.convert(value, AddressType) - @validator("contract_type", pre=True, always=True) - def fetch_contract_type(cls, value: Any, values: Dict[str, Any]) -> ContractType: + @field_validator("contract_type", mode="before") + @classmethod + def fetch_contract_type(cls, value: Any, info: ValidationInfo) -> Optional[ContractType]: # 0. If pre-loaded, default to that type if value: return value @@ -86,8 +90,8 @@ def fetch_contract_type(cls, value: Any, values: Dict[str, Any]) -> ContractType # 2. If contract cache has it, use that try: - if values.get("address") and ( - contract_type := cls.chain_manager.contracts.get(values["address"]) + if info.data.get("address") and ( + contract_type := cls.chain_manager.contracts.get(info.data["address"]) ): return contract_type @@ -95,10 +99,15 @@ def fetch_contract_type(cls, value: Any, values: Dict[str, Any]) -> ContractType pass # 3. Most expensive way is through package resources - cls._local_contracts = PackageManifest.parse_file( - importlib.resources.files("apepay") / "manifest.json" - ).contract_types - return cls._local_contracts["StreamManager"] + manifest_file = Path(__file__).parent / "manifest.json" + manifest_text = manifest_file.read_text() + manifest = PackageManifest.parse_raw(manifest_text) + + if not manifest or not manifest.contract_types: + raise ValueError("Invalid manifest") + + cls._local_contracts = manifest.contract_types + return cls._local_contracts.get("StreamManager") @property def contract(self) -> ContractInstance: @@ -157,7 +166,7 @@ def set_validators( def add_validators( self, - *new_validators: Iterable[_ValidatorItem], + *new_validators: _ValidatorItem, **txn_kwargs, ) -> ReceiptAPI: return self.set_validators( @@ -167,7 +176,7 @@ def add_validators( def remove_validators( self, - *validators: Iterable[_ValidatorItem], + *validators: _ValidatorItem, **txn_kwargs, ) -> ReceiptAPI: return self.set_validators( @@ -307,14 +316,16 @@ class Stream(BaseInterfaceModel): creation_receipt: Optional[ReceiptAPI] = None transaction_hash: Optional[HexBytes] = None - @validator("transaction_hash", pre=True) + @field_validator("transaction_hash", mode="before") + @classmethod def normalize_transaction_hash(cls, value: Any) -> Optional[HexBytes]: if value: return HexBytes(cls.conversion_manager.convert(value, bytes)) return value - @validator("creator", pre=True) + @field_validator("creator", mode="before") + @classmethod def validate_addresses(cls, value): return ( value if isinstance(value, str) else cls.conversion_manager.convert(value, AddressType) diff --git a/sdk/py/apepay/daemon.py b/sdk/py/apepay/daemon.py index 90fb9abe..55100285 100644 --- a/sdk/py/apepay/daemon.py +++ b/sdk/py/apepay/daemon.py @@ -4,9 +4,9 @@ from enum import Enum import click -from ape.types import AddressType from apepay import Stream, StreamManager -from silverback import SilverbackApp +from eth_utils import to_checksum_address +from silverback import SilverbackApp, SilverbackStartupState from .settings import Settings @@ -35,8 +35,9 @@ def from_time_left(cls, time_left: timedelta) -> "Status": SM = StreamManager( - address=os.environ.get("APEPAY_CONTRACT_ADDRESS") - or click.prompt("What address to use?", type=AddressType) + address=to_checksum_address( + os.environ.get("APEPAY_CONTRACT_ADDRESS") or click.prompt("What address to use?", type=str) + ) ) app = SilverbackApp() @@ -63,12 +64,12 @@ async def create_task_by_status(stream: Stream): @app.on_startup() -async def app_started(state): +async def app_started(startup_state: SilverbackStartupState): return await asyncio.gather( # Start watching all active streams and claim any completed but unclaimed streams *( create_task_by_status(stream) - for stream in SM.all_streams() + for stream in SM.all_streams(startup_state.last_block_seen) if stream.is_active or stream.amount_unlocked > 0 ) ) diff --git a/sdk/py/apepay/settings.py b/sdk/py/apepay/settings.py index 85cc500d..2305515c 100644 --- a/sdk/py/apepay/settings.py +++ b/sdk/py/apepay/settings.py @@ -2,14 +2,17 @@ from typing import Any from apepay.utils import time_unit_to_timedelta -from pydantic import BaseSettings, validator +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="APEPAY_", case_sensitive=True) + WARNING_LEVEL: timedelta = timedelta(days=2) CRITICAL_LEVEL: timedelta = timedelta(hours=12) - @validator("WARNING_LEVEL", "CRITICAL_LEVEL", pre=True) + @field_validator("WARNING_LEVEL", "CRITICAL_LEVEL", mode="before") def _normalize_timedelta(cls, value: Any) -> timedelta: if isinstance(value, timedelta): return value @@ -27,7 +30,3 @@ def _normalize_timedelta(cls, value: Any) -> timedelta: else: multiplier, time_unit = value.split(" ") return int(multiplier) * time_unit_to_timedelta(time_unit) - - class Config: - env_prefix = "APEPAY_" - case_sensitive = True diff --git a/sdk/py/apepay/utils.py b/sdk/py/apepay/utils.py index 13dae7c7..2b0b43de 100644 --- a/sdk/py/apepay/utils.py +++ b/sdk/py/apepay/utils.py @@ -7,7 +7,7 @@ def async_wrap_iter(it: Iterator) -> AsyncIterator: """Wrap blocking iterator into an asynchronous one""" loop = asyncio.get_event_loop() - q = asyncio.Queue(1) + q: asyncio.Queue = asyncio.Queue(1) exception = None _END = object()