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: support for ape 0.7, silverback 0.3, and pydantic 2 #91

Closed
wants to merge 5 commits into from
Closed
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Ape stuff
.build/
.cache/
sdk/py/apepay/manifest.json

# Python
.env
Expand Down
2,929 changes: 1,579 additions & 1,350 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
58 changes: 38 additions & 20 deletions sdk/py/apepay/__init__.py
Original file line number Diff line number Diff line change
@@ -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, model_validator

from .exceptions import (
FundsNotClaimable,
Expand All @@ -29,6 +31,8 @@
)
from .utils import time_unit_to_timedelta

SDict = Dict[str, Any]

MAX_DURATION_SECONDS = int(timedelta.max.total_seconds()) - 1


Expand All @@ -48,7 +52,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
Expand All @@ -63,14 +67,17 @@ 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:
@classmethod
def fetch_contract_type(
cls, value: Optional[Any] = None, address: Optional[AddressType] = None
) -> ContractType:
# 0. If pre-loaded, default to that type
if value:
return value
Expand All @@ -86,20 +93,29 @@ 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 address and (contract_type := cls.chain_manager.contracts.get(address)):
return contract_type

except Exception:
pass

# 3. Most expensive way is through package resources
cls._local_contracts = PackageManifest.parse_file(
importlib.resources.files("apepay") / "manifest.json"
).contract_types
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["StreamManager"]

# NOTE: self type notation because pydantic does some weird type massaging with this decorator
@model_validator(mode="after")
def validate_contract_type(self: "StreamManager") -> "StreamManager":
self.contract_type = StreamManager.fetch_contract_type(self.contract_type, self.address)
return self

@property
def contract(self) -> ContractInstance:
return self.chain_manager.contracts.instance_at(
Expand Down Expand Up @@ -157,7 +173,7 @@ def set_validators(

def add_validators(
self,
*new_validators: Iterable[_ValidatorItem],
*new_validators: _ValidatorItem,
**txn_kwargs,
) -> ReceiptAPI:
return self.set_validators(
Expand All @@ -167,7 +183,7 @@ def add_validators(

def remove_validators(
self,
*validators: Iterable[_ValidatorItem],
*validators: _ValidatorItem,
**txn_kwargs,
) -> ReceiptAPI:
return self.set_validators(
Expand Down Expand Up @@ -307,14 +323,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)
Expand Down
13 changes: 7 additions & 6 deletions sdk/py/apepay/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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
)
)
Expand Down
1 change: 1 addition & 0 deletions sdk/py/apepay/manifest.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions sdk/py/apepay/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion sdk/py/apepay/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down