Files
TwitchDropsMiner/utils.py
2022-02-27 12:07:40 +01:00

151 lines
4.0 KiB
Python

from __future__ import annotations
import random
import string
import asyncio
import logging
from functools import wraps
from contextlib import suppress
from datetime import datetime, timezone
from collections import abc, OrderedDict
from typing import Any, Literal, Generic, TypeVar, cast, TYPE_CHECKING
from constants import JsonType
if TYPE_CHECKING:
from typing_extensions import ParamSpec
else:
# stub it
class ParamSpec:
def __init__(*args, **kwargs):
pass
_T = TypeVar("_T") # type
_D = TypeVar("_D") # default
_P = ParamSpec("_P") # params
logger = logging.getLogger("TwitchDrops")
NONCE_CHARS = string.ascii_letters + string.digits
def timestamp(string: str) -> datetime:
return datetime.strptime(string, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
def create_nonce(length: int = 30) -> str:
return ''.join(random.choices(NONCE_CHARS, k=length))
def deduplicate(iterable: abc.Iterable[_T]) -> list[_T]:
return list(OrderedDict.fromkeys(iterable).keys())
def task_wrapper(
afunc: abc.Callable[_P, abc.Coroutine[Any, Any, _T]]
) -> abc.Callable[_P, abc.Coroutine[Any, Any, _T]]:
@wraps(afunc)
async def wrapper(*args: _P.args, **kwargs: _P.kwargs):
try:
await afunc(*args, **kwargs)
except Exception:
logger.exception("Exception in task")
raise # raise up to the wrapping task
return wrapper
def invalidate_cache(instance, *attrnames):
"""
To be used to invalidate `functools.cached_property`.
"""
for name in attrnames:
with suppress(AttributeError):
delattr(instance, name)
class OrderedSet(abc.MutableSet[_T]):
"""
Implementation of a set that preserves insertion order,
based on OrderedDict with values set to None.
"""
def __init__(self, iterable: abc.Iterable[_T] = [], /):
self._items: OrderedDict[_T, None] = OrderedDict((item, None) for item in iterable)
def __repr__(self) -> str:
return f"{self.__class__.__name__}([{', '.join(map(repr, self._items))}])"
def __contains__(self, item: object, /) -> bool:
return item in self._items
def __iter__(self) -> abc.Iterator[_T]:
return iter(self._items)
def __len__(self) -> int:
return len(self._items)
def add(self, item: _T, /) -> None:
self._items[item] = None
def discard(self, item: _T, /) -> None:
with suppress(KeyError):
del self._items[item]
def update(self, *others: abc.Iterable[_T]) -> None:
for it in others:
for item in it:
if item not in self._items:
self._items[item] = None
def difference_update(self, *others: abc.Iterable[_T]) -> None:
for it in others:
for item in it:
if item in self._items:
del self._items[item]
class AwaitableValue(Generic[_T]):
def __init__(self):
self._value: _T
self._event = asyncio.Event()
def has_value(self) -> bool:
return self._event.is_set()
def wait(self) -> abc.Coroutine[Any, Any, Literal[True]]:
return cast(abc.Coroutine[Any, Any, Literal[True]], self._event.wait())
def get_with_default(self, default: _D) -> _T | _D:
if self._event.is_set():
return self._value
return default
async def get(self) -> _T:
await self._event.wait()
return self._value
def set(self, value: _T) -> None:
self._value = value
self._event.set()
def clear(self) -> None:
self._event.clear()
class Game:
def __init__(self, data: JsonType):
self.id: int = int(data["id"])
self.name: str = data["name"]
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f"Game({self.id}, {self.name})"
def __eq__(self, other: object):
if isinstance(other, self.__class__):
return self.id == other.id
return NotImplemented
def __hash__(self) -> int:
return hash((self.__class__.__name__, self.id))