Source code for pons._provider
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
from contextlib import asynccontextmanager
from dataclasses import dataclass
from ethereum_rpc import RPCError
RPC_JSON = None | bool | int | float | str | Sequence["RPC_JSON"] | Mapping[str, "RPC_JSON"]
"""RPC requests and responses serializable to JSON."""
[docs]
class InvalidResponse(Exception):
"""Raised when the remote server's response is not of an expected format."""
[docs]
class Unreachable(Exception):
"""Raised when there is a problem connecting to the provider."""
[docs]
class ProtocolError(ABC, Exception):
"""
A protocol-specific error, indicating that the provider returned an error status
with no additional information allowing to categorize the error further.
See the provider-specifc derived class for this exception for more details.
"""
[docs]
@dataclass
class ProviderError(Exception):
"""Describes an error on the provider's side."""
error: RPCError | Unreachable | InvalidResponse | ProtocolError
"""The specific error."""
def __str__(self) -> str:
return f"Provider error: {self.error}"
[docs]
class ProviderPath:
"""Identifies a pinned provider."""
def __init__(self, path: Iterable[str]):
self._path = tuple(path)
def group(self, id_: str) -> "ProviderPath":
"""Prepends ``id_`` to the path."""
return ProviderPath((id_, *self._path))
def ungroup(self) -> "tuple[str, ProviderPath]":
"""Returns the top-level id and the subpath."""
return (self._path[0], ProviderPath(self._path[1:]))
@classmethod
def empty(cls) -> "ProviderPath":
"""Returns an empty path."""
return cls(())
def is_empty(self) -> bool:
"""Returns ``True`` if the path is empty."""
return not bool(self._path)
def __str__(self) -> str:
return "/".join(self._path)
def __repr__(self) -> str:
return f"ProviderPath({self._path!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, ProviderPath) and self._path == other._path
def __hash__(self) -> int:
return hash((ProviderPath, self._path))
[docs]
class Provider(ABC):
"""The base class for JSON RPC providers."""
@abstractmethod
@asynccontextmanager
async def session(self) -> AsyncIterator["ProviderSession"]:
"""
Opens a session to the provider
(allowing the backend to perform multiple operations faster).
"""
# mypy does not work with abstract generators correctly.
# See https://github.com/python/mypy/issues/5070
yield # type: ignore[misc]
class ProviderSession(ABC):
"""
The base class for provider sessions.
The methods of this class may raise :py:class:`ProviderError`
indicating a problem on the provider's side.
"""
@abstractmethod
async def rpc(self, method: str, *args: RPC_JSON) -> RPC_JSON:
"""Calls the given RPC method with the already json-ified arguments."""
...
async def rpc_and_pin(self, method: str, *args: RPC_JSON) -> tuple[RPC_JSON, ProviderPath]:
"""
Calls the given RPC method and returns the path to the provider it succeded on.
This method will be typically overriden by multi-provider implementations.
"""
return await self.rpc(method, *args), ProviderPath.empty()
async def rpc_at_pin(self, path: ProviderPath, method: str, *args: RPC_JSON) -> RPC_JSON:
"""
Calls the given RPC method at the provider by the given path
(obtained previously from ``rpc_and_pin()``).
This method will be typically overriden by multi-provider implementations.
"""
if not path.is_empty():
raise ValueError(f"Expected an empty provider path, got: `{path}`")
return await self.rpc(method, *args)