final
parent
61b4270332
commit
03de86597f
@ -0,0 +1,84 @@
|
||||
"""Comparison functions"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, List, Literal, Optional
|
||||
|
||||
|
||||
class Cmp(Enum):
|
||||
"""Comparison results"""
|
||||
|
||||
LT = -1
|
||||
EQ = 0
|
||||
GT = 1
|
||||
|
||||
|
||||
def deep_compare(val_a: Any, val_b: Any) -> Cmp:
|
||||
"""Compare two dictionaries key by key"""
|
||||
|
||||
if isinstance(val_a, dict):
|
||||
for key, elem_a in val_a.items():
|
||||
elem_b = val_b[key]
|
||||
|
||||
cmp = deep_compare(elem_a, elem_b)
|
||||
|
||||
if cmp == Cmp.EQ:
|
||||
continue
|
||||
|
||||
return cmp
|
||||
|
||||
return compare_arrays(list(val_a.keys()), list(val_b.keys()))
|
||||
|
||||
if val_a > val_b:
|
||||
return Cmp.GT
|
||||
|
||||
if val_a < val_b:
|
||||
return Cmp.LT
|
||||
|
||||
return Cmp.EQ
|
||||
|
||||
|
||||
def compare_basic(val_a: Any, val_b: Any, order: Literal['ASC', 'DESC'] = 'ASC') -> Cmp:
|
||||
"""Compare two basic values"""
|
||||
|
||||
cmp = deep_compare(val_a, val_b)
|
||||
|
||||
if cmp == Cmp.EQ:
|
||||
return Cmp.EQ
|
||||
|
||||
if order == 'ASC':
|
||||
return cmp
|
||||
|
||||
return Cmp.LT if cmp == Cmp.GT else Cmp.GT
|
||||
|
||||
|
||||
def compare_arrays(
|
||||
arr_a: List[Any],
|
||||
arr_b: List[Any],
|
||||
sort_orders: Optional[List[Literal['ASC', 'DESC']]] = None,
|
||||
) -> Cmp:
|
||||
"""Compare two arrays"""
|
||||
|
||||
sort_orders = sort_orders or []
|
||||
|
||||
for idx, (elem_a, elem_b) in enumerate(zip(arr_a, arr_b)):
|
||||
try:
|
||||
sort_order = sort_orders[idx]
|
||||
except IndexError:
|
||||
sort_order = 'ASC'
|
||||
|
||||
elem_cmp = compare_basic(elem_a, elem_b, sort_order)
|
||||
|
||||
if elem_cmp != Cmp.EQ:
|
||||
return elem_cmp
|
||||
|
||||
if len(arr_a) == len(arr_b):
|
||||
return Cmp.EQ
|
||||
|
||||
idx = min(len(arr_a), len(arr_b))
|
||||
|
||||
try:
|
||||
sort_order = sort_orders[idx]
|
||||
except IndexError:
|
||||
sort_order = 'ASC'
|
||||
|
||||
return compare_basic(len(arr_a), len(arr_b), sort_order)
|
@ -0,0 +1,6 @@
|
||||
from .syncer import SyncerBase
|
||||
|
||||
|
||||
class Peer:
|
||||
def __init__(self, syncer: SyncerBase) -> None:
|
||||
pass
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,118 @@
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Set, TypeVar
|
||||
|
||||
from .connection import Connection, ConnectionStatus
|
||||
from .envelope import Envelope, EnvelopeNotify, EnvelopeRequest
|
||||
from .util import make_id
|
||||
from .watchable import Watchable
|
||||
from . import TransportBase
|
||||
|
||||
T = TypeVar('T')
|
||||
Thunk = Callable[[], None]
|
||||
|
||||
|
||||
class Connection(Generic[T]):
|
||||
def __init__(self, transport: TransportBase, device_id: str, description: str, methods: T, send_envelope: Callable[[Connection[T], Envelope], None]) -> None:
|
||||
self.status = Watchable(ConnectionStatus.CONNECTING)
|
||||
self._close_cbs: Set[Thunk] = set()
|
||||
self.description = description
|
||||
self._transport = transport
|
||||
self._device_id = device_id
|
||||
self._other_device_id: Optional[str] = None
|
||||
self._methods = methods
|
||||
self._send_envelope = send_envelope
|
||||
self._deferred_requests: Dict[str, Deferred[Any]] = {}
|
||||
self._last_seen = 0
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
return self.status == ConnectionStatus.CLOSED
|
||||
|
||||
def on_close(self, func: Thunk) -> Thunk:
|
||||
if self.is_closed:
|
||||
raise RpcErrorUseAfterClose('the connection is closed')
|
||||
|
||||
self._close_cbs.add(func)
|
||||
|
||||
def del_cb():
|
||||
self._close_cbs.remove(func)
|
||||
|
||||
return del_cb
|
||||
|
||||
def close(self) -> None:
|
||||
if self.is_closed:
|
||||
return
|
||||
|
||||
self.status = ConnectionStatus.CLOSED
|
||||
|
||||
for func in self._close_cbs:
|
||||
func()
|
||||
|
||||
self._close_cbs.clear()
|
||||
|
||||
def handle_incoming_envelope(env: Envelope[T]) -> None:
|
||||
# TODO: maybe this function should be in a lock to ensure it only runs one at a time
|
||||
if self.is_closed:
|
||||
raise RpcErrorUseAfterClose('the connection is closed')
|
||||
|
||||
# TODO: throw error if status is ERROR?
|
||||
if env.kind == 'NOTIFY':
|
||||
if not hasattr(self._methods, env.method):
|
||||
logger.warn(f'> error in NOTIFY handler: no notify method called {env.method}')
|
||||
else:
|
||||
try:
|
||||
self._methods[env.method](*env.args)
|
||||
except BaseException as exc:
|
||||
logger.warn(f'> error when running NOTIFY method: {env} {exc}')
|
||||
|
||||
elif env.kind == 'REQUEST':
|
||||
try:
|
||||
if not hasattr(self._methods, env.method):
|
||||
raise RpcErrorUnknownMethod(f'unknown method in REQUEST: {env.method}')
|
||||
|
||||
data = self._methods[env.method](*env.args)
|
||||
response_env_data = EnvelopeResponseWithData(kind='RESPONSE', from_device_id=self._device_id, envelope_id=env.envelope_id, data=data)
|
||||
self._send_envelope(self, response_env_data)
|
||||
except BaseException as exc:
|
||||
response_env_error = EnvelopeResponseWithError(kind='RESPONSE', from_device_id=self.device_id, envelope_id=env.envelope_id, error=str(exc))
|
||||
|
||||
self._send_envelope(self, response_error)
|
||||
elif env.kind == 'RESPONSE':
|
||||
deferred = self._deferred_requests.get(env.envelope_id)
|
||||
|
||||
if not deferred:
|
||||
return
|
||||
|
||||
if env.data:
|
||||
deferred.resolve(env.data)
|
||||
elif env.error:
|
||||
deferred.reject(RpcErrorFromMethod(env.error))
|
||||
else:
|
||||
logger.warn('> RESPONSE has neither data nor error. This should never happen')
|
||||
|
||||
deferred.reject(RpcError('> RESPONSE has neither data nor error??'))
|
||||
|
||||
self._deferred_requests.remove(env.envelope_id)
|
||||
|
||||
def notify(self, method: MethodKey, *args: Parameters[BagType[MethodKey]]) -> None:
|
||||
if self.is_closed:
|
||||
raise RpcErrorUseAfterClose('the connection is closed')
|
||||
|
||||
env = EnvelopeNotify(
|
||||
kind='NOTIFY',
|
||||
from_device_id=self._device_id,
|
||||
envelope_id=f'env:{make_id()}',
|
||||
method=method,
|
||||
args=args
|
||||
)
|
||||
self._send_envelope(self, env)
|
||||
|
||||
def request(self, method: str, *args: Parameters[BagType[MethodKey]]) -> ReturnType[BagType[MethodKey]]:
|
||||
if self.is_closed:
|
||||
raise RpcErrorUseAfterClose('the connection is closed')
|
||||
|
||||
env = EnvelopeRequest(kind='REQUEST', from_device_id=self._device_id, envelope_id=f'env:{make_id}', method=method, args=args)
|
||||
deferred = make_deferred()
|
||||
self._deferred_requests[env.envelope_id] = deferred
|
||||
self._send_envelope(self, env)
|
||||
|
||||
return deferred.promise
|
@ -0,0 +1,115 @@
|
||||
"""RPC Envelope handling"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Callable, Dict, List, Literal, Union
|
||||
|
||||
Fn = Callable[..., Any]
|
||||
FnsBag = Dict[str, Fn]
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class EnvelopeKind(Enum):
|
||||
"""Types of envelopes"""
|
||||
|
||||
NOTIFY = auto()
|
||||
REQUEST = auto()
|
||||
RESPONSE = auto()
|
||||
|
||||
|
||||
class EnvelopeBase:
|
||||
"""Base envelope type"""
|
||||
|
||||
kind: EnvelopeKind
|
||||
from_device_id: str
|
||||
envelope_id: str
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""Convert the envelope to a dictionary"""
|
||||
|
||||
return {
|
||||
'kind': self.kind.name,
|
||||
'fromDeviceId': self.from_device_id,
|
||||
'envelopeId': self.envelope_id,
|
||||
}
|
||||
|
||||
|
||||
class EnvelopeNotify(EnvelopeBase):
|
||||
"""Envelope type for a notification"""
|
||||
|
||||
kind: Literal[EnvelopeKind.NOTIFY]
|
||||
method: str
|
||||
args: List[str]
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
envelope = super().as_dict()
|
||||
envelope.update(
|
||||
{
|
||||
'method': self.method,
|
||||
'args': self.args,
|
||||
}
|
||||
)
|
||||
|
||||
return envelope
|
||||
|
||||
|
||||
class EnvelopeRequest(EnvelopeBase):
|
||||
"""Envelope type for a request"""
|
||||
|
||||
kind: Literal[EnvelopeKind.REQUEST]
|
||||
method: str
|
||||
args: List[str]
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
envelope = super().as_dict()
|
||||
envelope.update(
|
||||
{
|
||||
'method': self.method,
|
||||
'args': self.args,
|
||||
}
|
||||
)
|
||||
|
||||
return envelope
|
||||
|
||||
|
||||
class EnvelopeResponseWithData(EnvelopeBase):
|
||||
"""Envelope type for a data response"""
|
||||
|
||||
kind: Literal[EnvelopeKind.RESPONSE]
|
||||
data: List[str]
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
envelope = super().as_dict()
|
||||
envelope.update(
|
||||
{
|
||||
'data': self.data,
|
||||
}
|
||||
)
|
||||
|
||||
return envelope
|
||||
|
||||
|
||||
class EnvelopeResponseWithError(EnvelopeBase):
|
||||
"""Envelope type for an error response"""
|
||||
|
||||
kind: Literal[EnvelopeKind.RESPONSE]
|
||||
error: str
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
envelope = super().as_dict()
|
||||
envelope.update(
|
||||
{
|
||||
'error': self.error,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
EvelopeResponse = Union[EnvelopeResponseWithData, EnvelopeResponseWithError]
|
||||
|
||||
|
||||
Envelope = Union[
|
||||
EnvelopeNotify,
|
||||
EnvelopeRequest,
|
||||
EnvelopeResponseWithData,
|
||||
EnvelopeResponseWithError,
|
||||
]
|
@ -0,0 +1,2 @@
|
||||
def make_id() -> str:
|
||||
return str(randint(0, 999999999999999)).zfill(15)
|
@ -1,8 +1,15 @@
|
||||
"""Generic types and definitions
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'
|
||||
ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
DIGIT = '0123456789'
|
||||
B32_CHAR = ALPHA_LOWER + '234567'
|
||||
ALPHA_LOWER_OR_DIGIT = ALPHA_LOWER + DIGIT
|
||||
PRINTABLE_ASCII = bytes(range(32, 127)).decode('utf-8')
|
||||
|
||||
ASCII = bytes(range(128))
|
||||
|
||||
Cmp = Literal[-1, 0, 1]
|
||||
|
@ -0,0 +1,11 @@
|
||||
from datetime import datetime
|
||||
from random import random
|
||||
|
||||
|
||||
def microsecond_now() -> int:
|
||||
return int(datetime.utcnow().timestamp() * 1000)
|
||||
|
||||
|
||||
def random_id() -> str:
|
||||
# TODO: better randomness here
|
||||
return f'{random()}{random()}'
|
Loading…
Reference in New Issue