diff --git a/docs/changelog/v5.md b/docs/changelog/v5.md index 94d6e256..fefcfa7a 100644 --- a/docs/changelog/v5.md +++ b/docs/changelog/v5.md @@ -1,5 +1,17 @@ # 5.x.x +## 5.3.0 + +- New `pyot.utils.functools` module. +- New utilities `async_property` and `async_cached_property` in `pyot.utils.functools`. +- New utility `sync_to_async` in `pyot.utils.sync`. +- Removed `PtrCache` from `pyot.utils.cache`, use `functools.lru_cache` instead. +- Removed `cached_property` from `pyot.utils.cache`, use `functools.cached_property` instead. +- Removed `pyot.utils.cache` module. +- Reworked `pyot.utils.lol.champion` (interfaces unchanged). +- Fixed some typings in util modules. +- Fixed a bug where `pyot.core.functional.lazy_property` is not caching returned values. + ## 5.2.0 - `lol.MerakiItem` removed `meraki_` prefixed properties, non-prefixed properties will replace them. To keep having access to `lol.Item`s properties, use the `item` property instead. diff --git a/docs/cores/configuration.md b/docs/cores/configuration.md index 74afdb0d..98354e09 100644 --- a/docs/cores/configuration.md +++ b/docs/cores/configuration.md @@ -9,6 +9,10 @@ These configurations generally stays all packed in a single file (generally call - Adding `pyot` to Django's `INSTALLED_APPS` by following Django integration guide. - Or any way that would cause imports on these configurations files. +{% hint style='tip' %} +If an integration of Pyot to another framework is needed. General integration guides are provided for Django, FastAPI and Celery projects at the **Integrations** page. These guides does not require to be strictly followed and only serves for reference purposes. +{% endhint %} + ## Model Conf Module: `pyot.conf.model` diff --git a/docs/cores/installation.md b/docs/cores/installation.md index 693f5628..18a779c5 100644 --- a/docs/cores/installation.md +++ b/docs/cores/installation.md @@ -25,8 +25,8 @@ pip install git+https://github.com/paaksing/Pyot.git Depending on the need, installation of extras may be needed: ```shell -pip install pyot[diskcache] # installs: ["diskcache>=5.1", "asgiref>=3.2"] -pip install pyot[redis] # installs: ["aioredis<2.0"] -pip install pyot[mongodb] # installs: ["motor>=2.3"] -pip install pyot[test] # installs: ["typeguard>=2.13"] +pip install pyot[diskcache] # installs: ["diskcache>=5.1", "asgiref>=3.2"] +pip install pyot[redis] # installs: ["aioredis<2.0"] +pip install pyot[mongodb] # installs: ["motor>=2.3"] +pip install pyot[test] # installs: ["typeguard>=2.13"] ``` diff --git a/docs/cores/objects.md b/docs/cores/objects.md index 4f723421..ddfde84f 100644 --- a/docs/cores/objects.md +++ b/docs/cores/objects.md @@ -17,6 +17,7 @@ Some internal info has been hidden, to learn more please review source code inst ### _class_ PyotStaticBase Metaclass: `PyotMetaClass` + Extends: `PyotRoutingBase` Definitions: @@ -51,7 +52,6 @@ Methods: ## Pyot Core Base class: `PyotCoreBase` -Extend class: `PyotStaticBase` Inherits all functionalities of `PyotStaticBase`. This type of objects has the ability to request for data on pipelines. diff --git a/docs/utils/cache.md b/docs/utils/cache.md deleted file mode 100644 index 8f94c71b..00000000 --- a/docs/utils/cache.md +++ /dev/null @@ -1,93 +0,0 @@ -# Cache - -Module: `pyot.utils.cache` - -### _class_ PtrCache - -> A high performance mini local cache based on reference keeping. -> Be aware that this cache is NOT isolated, hence the performance difference from Omnistone. -> This cache will not copy the objects on get/put, modification to objects affects cached objects. -> -> You can pass a class to instantiation param `class_of_t` for typing and autocompletion. - -Extends: -* `Generic` - -Definitions: -* `__init__` -> `None` - * `expiration`: `int = 10800` - * `max_entries`: `int = 5000` - * `class_of_t`: `Union[Type[~T], NoneType] = None` -* `__new__` -> `None` - * `cls`: `None` - * `args`: `None` - * `kwds`: `None` - -Methods: -* _asyncmethod_ `aget` -> `~T` - * `name`: `str` - * `coro`: `None` - * `lazy`: `bool = False` - > Async get an object from the cache. - > - > `coro` will be awaited when provided and if object doesn't exist, set the returned value before returning. - > - > `lazy` flag if `coro` needs to be called before running it, therefore achieve lazy eval during runtime. - > - > - > If the `coro` doesn't need to be awaited it will be closed and not raise warnings. -* _method_ `clear` -> `None` - > Clear the cache. -* _method_ `get` -> `~T` - * `name`: `str` - * `func`: `None` - * `lazy`: `bool = False` - > Get an object from the cache. - > - > `func` will be called when provided and if object doesn't exist, set the returned value before returning. - > - > `lazy` flag if `func` needs to be called before running it, therefore achieve lazy eval during runtime. -* _method_ `set` -> `None` - * `name`: `str` - * `val`: `~T` - * `exp`: `int = None` - > Put an object to the cache. - -Attributes: -* `objects` -> `dict` -* `max_entries` -> `int` - - -### _class_ cached_property - -> Decorator that converts a method with a single self argument into a -> property cached on the instance. -> A cached property can be made out of an existing method: -> (e.g. ``url = cached_property(get_absolute_url)``). - -Extends: -* `Generic` - -Definitions: -* `__get__` -> `~R` - * `instance`: `None` - * `cls`: `None` -* `__init__` -> `None` - * `func`: `Callable[..., ~R]` - * `name`: `None` -* `__new__` -> `None` - * `cls`: `None` - * `args`: `None` - * `kwds`: `None` -* `__set_name__` -> `None` - * `owner`: `None` - * `name`: `None` - -Methods: -* _method_ `func` -> `~R` - * `instance`: `None` - - -### _alias_ `ptr_cache` ~ `PtrCache` - - diff --git a/docs/utils/functools.md b/docs/utils/functools.md new file mode 100644 index 00000000..706384b3 --- /dev/null +++ b/docs/utils/functools.md @@ -0,0 +1,89 @@ +# Functools + +Module: `pyot.utils.functools` + +### _class_ async_cached_property + +> Async equivalent of `functools.cached_property`, takes an async method and +> converts it to a cached property that returns an awaitable with the return value. +> +> Usage: +> ``` +> class A: +> @async_cached_property +> async def b(self): +> ... +> +> a = A() +> await a.b +> ``` + +Extends: +* `pyot.utils.functools.async_property` +* `Generic` + +Definitions: +* `__get__` -> `Awaitable[~R]` + * `instance`: `None` + * `cls`: `None` +* `__init__` -> `None` + * `func`: `Callable[..., Awaitable[~R]]` + * `name`: `None` +* `__new__` -> `None` + * `cls`: `None` + * `args`: `None` + * `kwds`: `None` +* `__set__` -> `None` + * `obj`: `None` + * `value`: `None` +* `__set_name__` -> `None` + * `owner`: `None` + * `name`: `None` + +Methods: +* _asyncmethod_ `proxy` -> `Awaitable[~R]` + * `instance`: `None` + + +### _class_ async_property + +> Async equivalent of `property`, takes an async method and +> converts it to a property that returns an awaitable with the return value. +> +> Usage: +> ``` +> class A: +> @async_property +> async def b(self): +> ... +> +> a = A() +> await a.b +> ``` + +Extends: +* `Generic` + +Definitions: +* `__get__` -> `Awaitable[~R]` + * `instance`: `None` + * `cls`: `None` +* `__init__` -> `None` + * `func`: `Callable[..., Awaitable[~R]]` + * `name`: `None` +* `__new__` -> `None` + * `cls`: `None` + * `args`: `None` + * `kwds`: `None` +* `__set__` -> `None` + * `obj`: `None` + * `value`: `None` +* `__set_name__` -> `None` + * `owner`: `None` + * `name`: `None` + +Methods: +* _asyncmethod_ `proxy` -> `Awaitable[~R]` + * `instance`: `None` + + diff --git a/docs/utils/lol/champion.md b/docs/utils/lol/champion.md index b0e4b5e7..c5d91961 100644 --- a/docs/utils/lol/champion.md +++ b/docs/utils/lol/champion.md @@ -2,38 +2,43 @@ Module: `pyot.utils.lol.champion` -### _asyncfunction_ `fill_champion_summary` -> `None` -* `cache`: `pyot.utils.cache.PtrCache` -> Fill champion summary data to cache. +### _class_ ChampionKeysCache +Definitions: +* `__init__` -> `None` +* `__str__` -> `str` -### _asyncfunction_ `id_by_key` -> `None` -* `value`: `None` + +### _constant_ `champion_keys_cache`: `ChampionKeysCache()` + + +### _asyncfunction_ `id_by_key` -> `int` +* `value`: `str` > Get champion id by key -### _asyncfunction_ `id_by_name` -> `None` -* `value`: `None` +### _asyncfunction_ `id_by_name` -> `int` +* `value`: `str` > Get champion id by name -### _asyncfunction_ `key_by_id` -> `None` -* `value`: `None` +### _asyncfunction_ `key_by_id` -> `str` +* `value`: `int` > Get champion key by id -### _asyncfunction_ `key_by_name` -> `None` -* `value`: `None` +### _asyncfunction_ `key_by_name` -> `str` +* `value`: `str` > Get champion key by name -### _asyncfunction_ `name_by_id` -> `None` -* `value`: `None` +### _asyncfunction_ `name_by_id` -> `str` +* `value`: `int` > Get champion name by id -### _asyncfunction_ `name_by_key` -> `None` -* `value`: `None` +### _asyncfunction_ `name_by_key` -> `str` +* `value`: `str` > Get champion name by key diff --git a/docs/utils/parsers.md b/docs/utils/parsers.md index 8841b8e4..824f48b5 100644 --- a/docs/utils/parsers.md +++ b/docs/utils/parsers.md @@ -3,18 +3,18 @@ Module: `pyot.utils.parsers` ### _function_ `from_bytes` -> `~T` -* `obj`: `None` +* `obj`: `bytes` * `class_of_t`: `Union[Type[~T], NoneType] = None` > Convert a byte string to python object. -### _function_ `safejson` -> `None` +### _function_ `safejson` -> `Any` * `content`: `str` > Same as json.loads with graceful fallback by returning original string -### _function_ `to_bytes` -> `ByteString` -* `obj`: `None` +### _function_ `to_bytes` -> `bytes` +* `obj`: `Any` > Convert a python object to byte string. diff --git a/docs/utils/runners.md b/docs/utils/runners.md index 16f4ff94..526fa795 100644 --- a/docs/utils/runners.md +++ b/docs/utils/runners.md @@ -9,8 +9,8 @@ Module: `pyot.utils.runners` ### _asyncfunction_ `thread_run` -> `~R` * `func`: `Callable[..., ~R]` -* `args`: `None` -* `kwargs`: `None` +* `args`: `Sequence[Any]` +* `kwargs`: `Mapping[str, Any]` > Run a blocking function in a thread. diff --git a/docs/utils/sync.md b/docs/utils/sync.md index f45d1d53..a428dd33 100644 --- a/docs/utils/sync.md +++ b/docs/utils/sync.md @@ -4,6 +4,11 @@ Module: `pyot.utils.sync` ### _function_ `async_to_sync` -> `Callable[..., ~R]` * `func`: `Callable[..., Awaitable[~R]]` -> Wraps `asyncio.run` on an async function making it sync callable. Can be used as decorator @async_to_sync +> Wraps `asyncio.run` on an async function converting to sync callable. Can be used as decorator @async_to_sync + + +### _function_ `sync_to_async` -> `Callable[..., Awaitable[~R]]` +* `func`: `Callable[..., ~R]` +> Wraps `thread_run` on a blocking function converting to async by running in a thread. Can be used as decorator @sync_to_async diff --git a/pyot/core/functional.py b/pyot/core/functional.py index 6f3b5d1a..e87a6231 100644 --- a/pyot/core/functional.py +++ b/pyot/core/functional.py @@ -1,4 +1,4 @@ -from typing import Dict, Union +from typing import Any, Dict, Union from functools import partial, wraps from pyot.utils.text import camelcase @@ -8,25 +8,28 @@ class lazy_property(property): """ Decorator that converts a method with a single self argument into a - property cached on the instance and inserted into the meta data dict using camelcase key. - A cached property can be made out of an existing method: - (e.g. ``url = lazy_property(get_absolute_url)``). + property cached on the instance and inserted into the meta data dict using camelcase key.\n + Can only be used on pyot class methods. """ name = None - @staticmethod - def func(instance): # pylint: disable=method-hidden + @classmethod + def func(cls, instance) -> Any: raise TypeError( - 'Cannot use lazy_property instance without calling ' + f'Cannot use {cls.__class__.__name__} instance without calling ' '__set_name__() on it.' ) def __init__(self, func, name=None): # pylint: disable=super-init-not-called self.real_func = func self.key = camelcase(func.__name__) + self.once = False self.__doc__ = getattr(func, '__doc__') def __set_name__(self, owner, name): + from .objects import PyotStaticBase + if not issubclass(owner, PyotStaticBase): + raise TypeError('Cannot use lazy_property on non pyot class methods.') if self.name is None: self.name = name self.func = self.real_func @@ -37,13 +40,14 @@ def __set_name__(self, owner, name): ) def __get__(self, instance, cls=None): - """ - Call the function and put the return value in instance.__dict__ so that - subsequent attribute access on the instance returns the cached value - instead of calling cached_property.__get__(). - """ if instance is None: return self + if self.once: + try: + return instance._meta.data[self.key] + except KeyError: + pass + self.once = True res = instance._meta.data[self.key] = instance.__dict__[self.name] = self.func(instance) return res diff --git a/pyot/utils/cache.py b/pyot/utils/cache.py deleted file mode 100644 index 95ad7d30..00000000 --- a/pyot/utils/cache.py +++ /dev/null @@ -1,134 +0,0 @@ -from datetime import datetime, timedelta -from typing import TypeVar, Callable, Generic, Optional, Type - - -T = TypeVar("T") -R = TypeVar("R") - - -class PtrCache(Generic[T]): - ''' - A high performance mini local cache based on reference keeping. - Be aware that this cache is NOT isolated, hence the performance difference from Omnistone. - This cache will not copy the objects on get/put, modification to objects affects cached objects. - - You can pass a class to instantiation param `class_of_t` for typing and autocompletion. - ''' - objects: dict - max_entries: int - - def __init__(self, expiration=60*60*3, max_entries=5000, class_of_t: Optional[Type[T]] = None): - self.objects = {} - self.expiration = expiration - self.max_entries = max_entries - - def get(self, name: str, func=None, lazy: bool = False) -> T: - ''' - Get an object from the cache. - - `func` will be called when provided and if object doesn't exist, set the returned value before returning.\n - `lazy` flag if `func` needs to be called before running it, therefore achieve lazy eval during runtime. - ''' - try: - data = self.objects[name] - if data[1] is not None and data[1] < datetime.now(): - del self.objects[name] - raise KeyError(name) - return data[0] - except KeyError: - if func is None: - return None - response = func()() if lazy else func() - self.set(name, response) - return response - - async def aget(self, name: str, coro=None, lazy: bool = False) -> T: - ''' - Async get an object from the cache. - - `coro` will be awaited when provided and if object doesn't exist, set the returned value before returning.\n - `lazy` flag if `coro` needs to be called before running it, therefore achieve lazy eval during runtime.\n - - If the `coro` doesn't need to be awaited it will be closed and not raise warnings. - ''' - try: - data = self.objects[name] - if data[1] is not None and data[1] < datetime.now(): - del self.objects[name] - raise KeyError(name) - if not lazy and coro: - coro.close() - return data[0] - except KeyError: - if coro is None: - return None - response = (await coro()) if lazy else (await coro) - self.set(name, response) - return response - - def set(self, name: str, val: T, exp: int = None): - '''Put an object to the cache.''' - if exp is None: - exp = self.expiration - if exp >= 0: - self.objects[name] = [val, datetime.now() + timedelta(seconds=exp)] - else: - self.objects[name] = [val, None] - if len(self.objects) > self.max_entries: - number = 0 - for key in self.objects: - if number < self.max_entries/2: - del self.objects[key] - number += 1 - continue - break - return name - - def clear(self): - '''Clear the cache.''' - self.objects = dict() - - -ptr_cache = PtrCache - - -class cached_property(Generic[R]): - """ - Decorator that converts a method with a single self argument into a - property cached on the instance. - A cached property can be made out of an existing method: - (e.g. ``url = cached_property(get_absolute_url)``). - """ - name = None - - @staticmethod - def func(instance) -> R: # pylint: disable=method-hidden - raise TypeError( - 'Cannot use cached_property instance without calling ' - '__set_name__() on it.' - ) - - def __init__(self, func: Callable[..., R], name=None): - self.real_func = func - self.__doc__ = getattr(func, '__doc__') - - def __set_name__(self, owner, name): - if self.name is None: - self.name = name - self.func = self.real_func - elif name != self.name: - raise TypeError( - "Cannot assign the same cached_property to two different names " - "(%r and %r)." % (self.name, name) - ) - - def __get__(self, instance, cls=None) -> R: - """ - Call the function and put the return value in instance.__dict__ so that - subsequent attribute access on the instance returns the cached value - instead of calling cached_property.__get__(). - """ - if instance is None: - return self - res = instance.__dict__[self.name] = self.func(instance) - return res diff --git a/pyot/utils/functools.py b/pyot/utils/functools.py new file mode 100644 index 00000000..5dac5c88 --- /dev/null +++ b/pyot/utils/functools.py @@ -0,0 +1,95 @@ + + +from asyncio import iscoroutinefunction +from typing import Any, Awaitable, Callable, Generic, TypeVar + + +R = TypeVar("R") + + +class async_property(Generic[R]): + ''' + Async equivalent of `property`, takes an async method and + converts it to a property that returns an awaitable with the return value. + + Usage: + ``` + class A: + @async_property + async def b(self): + ... + + a = A() + await a.b + ``` + ''' + name = None + + @classmethod + def func(cls, instance) -> Any: + raise TypeError( + f'Cannot use {cls.__class__.__name__} instance without calling ' + '__set_name__() on it.' + ) + + def __init__(self, func: Callable[..., Awaitable[R]], name=None): + assert iscoroutinefunction(func), "Cannot use on non-async functions" + self.real_func = func + self.__doc__ = getattr(func, '__doc__') + + def __set_name__(self, owner, name): + if self.name is None: + self.name = name + self.func = self.real_func + elif name != self.name: + raise TypeError( + f"Cannot assign the same {self.__class__.__name__} to two different names " + f"({self.name} and {name})." + ) + + def __get__(self, instance, cls=None) -> Awaitable[R]: + if instance is None: + return self + return self.proxy(instance) + + def __set__(self, obj, value): + raise AttributeError("can't set attribute") + + async def proxy(self, instance) -> Awaitable[R]: + res = await self.func(instance) + return res + + +class async_cached_property(async_property, Generic[R]): + ''' + Async equivalent of `functools.cached_property`, takes an async method and + converts it to a cached property that returns an awaitable with the return value. + + Usage: + ``` + class A: + @async_cached_property + async def b(self): + ... + + a = A() + await a.b + ``` + ''' + name = None + + def __init__(self, func: Callable[..., Awaitable[R]], name=None): + assert iscoroutinefunction(func), "Cannot use on non-async functions" + self.real_func = func + self.once = False + self.__doc__ = getattr(func, '__doc__') + + async def proxy(self, instance) -> Awaitable[R]: + if self.once: + try: + return instance.__dict__[self.name] + except KeyError: + pass + self.once = True + res = instance.__dict__[self.name] = await self.func(instance) + return res diff --git a/pyot/utils/lol/champion.py b/pyot/utils/lol/champion.py index 15b455d7..201b0a40 100644 --- a/pyot/utils/lol/champion.py +++ b/pyot/utils/lol/champion.py @@ -1,18 +1,41 @@ -import aiohttp -from ..cache import PtrCache - +from datetime import datetime, timedelta +from ..functools import async_property -CHAMPION_SUMMARY = PtrCache() +import asyncio +import aiohttp -async def fill_champion_summary(cache: PtrCache): - '''Fill champion summary data to cache.''' - url = "https://raw.communitydragon.org/pbe/plugins/rcp-be-lol-game-data/global/en_gb/v1/champion-summary.json" - async with aiohttp.ClientSession() as session: - response = await session.request("GET", url) - if response and response.status == 200: - dic = await response.json(encoding="utf-8") - transformers = { +class ChampionKeysCache: + + def __init__(self) -> None: + self.cached_data = { + "id_by_key": {}, + "id_by_name": {}, + "key_by_id": {}, + "key_by_name": {}, + "name_by_id": {}, + "name_by_key": {}, + } + self.lock = asyncio.Lock() + self.last_updated = datetime.now() - timedelta(days=1) + + def __str__(self) -> str: + return 'ChampionKeysCache()' + + @async_property + async def data(self): + if datetime.now() - self.last_updated < timedelta(hours=3): + return self.cached_data + async with self.lock: + if datetime.now() - self.last_updated < timedelta(hours=3): + return self.cached_data + url = "https://raw.communitydragon.org/pbe/plugins/rcp-be-lol-game-data/global/en_gb/v1/champion-summary.json" + async with aiohttp.ClientSession() as session: + response = await session.request("GET", url) + if not (response and response.status == 200): + raise RuntimeError(f"Failed to pull champion summary ({response.status})") + dic = await response.json(encoding="utf-8") + data = { "id_by_key": {}, "id_by_name": {}, "key_by_id": {}, @@ -23,67 +46,51 @@ async def fill_champion_summary(cache: PtrCache): for champ in dic: if champ["id"] == -1: continue - transformers["id_by_key"][champ["alias"]] = champ["id"] - transformers["id_by_name"][champ["name"]] = champ["id"] - transformers["key_by_id"][champ["id"]] = champ["alias"] - transformers["key_by_name"][champ["name"]] = champ["alias"] - transformers["name_by_id"][champ["id"]] = champ["name"] - transformers["name_by_key"][champ["alias"]] = champ["name"] - for key, val in transformers.items(): - cache.set(key, val) - else: - raise RuntimeError("Unable to pull champion summary") - - -async def id_by_key(value): + data["id_by_key"][champ["alias"]] = champ["id"] + data["id_by_name"][champ["name"]] = champ["id"] + data["key_by_id"][champ["id"]] = champ["alias"] + data["key_by_name"][champ["name"]] = champ["alias"] + data["name_by_id"][champ["id"]] = champ["name"] + data["name_by_key"][champ["alias"]] = champ["name"] + self.cached_data = data + self.last_updated = datetime.now() + return self.cached_data + + +champion_keys_cache = ChampionKeysCache() + + +async def id_by_key(value: str) -> int: '''Get champion id by key''' - data = CHAMPION_SUMMARY.get("id_by_key") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("id_by_key") - return data[value] + data = await champion_keys_cache.data + return data["id_by_key"][value] -async def id_by_name(value): +async def id_by_name(value: str) -> int: '''Get champion id by name''' - data = CHAMPION_SUMMARY.get("id_by_name") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("id_by_name") - return data[value] + data = await champion_keys_cache.data + return data["id_by_name"][value] -async def key_by_id(value): +async def key_by_id(value: int) -> str: '''Get champion key by id''' - data = CHAMPION_SUMMARY.get("key_by_id") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("key_by_id") - return data[value] + data = await champion_keys_cache.data + return data["key_by_id"][value] -async def key_by_name(value): +async def key_by_name(value: str) -> str: '''Get champion key by name''' - data = CHAMPION_SUMMARY.get("key_by_name") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("key_by_name") - return data[value] + data = await champion_keys_cache.data + return data["key_by_name"][value] -async def name_by_id(value): +async def name_by_id(value: int) -> str: '''Get champion name by id''' - data = CHAMPION_SUMMARY.get("name_by_id") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("name_by_id") - return data[value] + data = await champion_keys_cache.data + return data["name_by_id"][value] -async def name_by_key(value): +async def name_by_key(value: str) -> str: '''Get champion name by key''' - data = CHAMPION_SUMMARY.get("name_by_key") - if data is None: - await fill_champion_summary(CHAMPION_SUMMARY) - data = CHAMPION_SUMMARY.get("name_by_key") - return data[value] + data = await champion_keys_cache.data + return data["name_by_key"][value] diff --git a/pyot/utils/parsers.py b/pyot/utils/parsers.py index d16247bd..22e52351 100644 --- a/pyot/utils/parsers.py +++ b/pyot/utils/parsers.py @@ -1,4 +1,4 @@ -from typing import ByteString, Optional, Type, TypeVar +from typing import Any, Optional, Type, TypeVar import pickle import json @@ -6,17 +6,17 @@ T = TypeVar("T") -def to_bytes(obj) -> ByteString: +def to_bytes(obj: Any) -> bytes: '''Convert a python object to byte string.''' return pickle.dumps(obj) -def from_bytes(obj, class_of_t: Optional[Type[T]] = None) -> T: +def from_bytes(obj: bytes, class_of_t: Optional[Type[T]] = None) -> T: '''Convert a byte string to python object.''' return pickle.loads(obj) -def safejson(content: str): +def safejson(content: str) -> Any: '''Same as json.loads with graceful fallback by returning original string''' try: return json.loads(content) diff --git a/pyot/utils/runners.py b/pyot/utils/runners.py index 5788b0e4..a10b143b 100644 --- a/pyot/utils/runners.py +++ b/pyot/utils/runners.py @@ -1,5 +1,5 @@ from functools import partial -from typing import TypeVar, Callable, Awaitable +from typing import Any, Mapping, Sequence, TypeVar, Callable, Awaitable import asyncio @@ -11,7 +11,7 @@ def loop_run(coro: Awaitable[R]) -> R: return asyncio.get_event_loop().run_until_complete(coro) -async def thread_run(func: Callable[..., R], *args, **kwargs) -> R: +async def thread_run(func: Callable[..., R], *args: Sequence[Any], **kwargs: Mapping[str, Any]) -> R: '''Run a blocking function in a thread.''' loop = asyncio.get_event_loop() return await loop.run_in_executor(None, partial(func, *args, **kwargs)) diff --git a/pyot/utils/sync.py b/pyot/utils/sync.py index 551286df..87b3b08c 100644 --- a/pyot/utils/sync.py +++ b/pyot/utils/sync.py @@ -2,15 +2,25 @@ from functools import wraps import asyncio +from .runners import thread_run + R = TypeVar("R") def async_to_sync(func: Callable[..., Awaitable[R]]) -> Callable[..., R]: - '''Wraps `asyncio.run` on an async function making it sync callable. Can be used as decorator @async_to_sync''' + '''Wraps `asyncio.run` on an async function converting to sync callable. Can be used as decorator @async_to_sync''' if not asyncio.iscoroutinefunction(func): raise TypeError(f"{func} is not a coroutine function") @wraps(func) def wrapper(*args, **kwargs): return asyncio.run(func(*args, **kwargs)) return wrapper + + +def sync_to_async(func: Callable[..., R]) -> Callable[..., Awaitable[R]]: + '''Wraps `thread_run` on a blocking function converting to async by running in a thread. Can be used as decorator @sync_to_async''' + @wraps(func) + async def wrapper(*args, **kwargs): + return await thread_run(func, *args, **kwargs) + return wrapper diff --git a/setup.py b/setup.py index ea001475..130e4c3c 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( name="pyot", - version="5.2.0", + version="5.3.0", author="Paaksing", author_email="paaksingtech@gmail.com", url="https://github.com/paaksing/Pyot",