From 534cd2f714c4ae36858effe4b1cd80389fafb215 Mon Sep 17 00:00:00 2001 From: Galtozzy Date: Thu, 12 Sep 2024 02:42:06 +0200 Subject: [PATCH 1/5] WIP lock refactor --- py_cachify/backend/lock.py | 147 ++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index 3bb0fb1..e53b5bc 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -2,7 +2,19 @@ import logging from contextlib import asynccontextmanager, contextmanager from functools import partial, wraps -from typing import Any, AsyncGenerator, Awaitable, Callable, Generator, TypeVar, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Awaitable, + Callable, + Generator, + Optional, + Protocol, + TypeVar, + Union, + cast, +) from typing_extensions import ParamSpec, deprecated, overload @@ -12,6 +24,10 @@ from .types import AsyncWithResetProtocol, SyncOrAsync, SyncWithResetProtocol +if TYPE_CHECKING: + from .lib import Cachify + + logger = logging.getLogger(__name__) R = TypeVar('R') P = ParamSpec('P') @@ -25,6 +41,135 @@ def _check_is_cached(is_already_cached: bool, key: str) -> None: raise CachifyLockError(msg) +class LockProtocolBase(Protocol): + _cachify: 'Cachify' + _key: str + _reentrant: bool + _nowait: bool + _timeout: Optional[int] + + @staticmethod + def _check_is_cached(is_already_cached: bool, key: str) -> None: ... + + +class AsyncLockMethods(LockProtocolBase): + async def _a_acquire(self, key: str) -> None: + cached = await self._cachify.a_get(key=key) + self._check_is_cached(is_already_cached=bool(cached), key=key) + + await self._cachify.a_set(key=key, val=1) + + async def _a_release(self, key: str) -> None: + await self._cachify.a_delete(key=key) + + async def __aenter__(self, key: Optional[str] = None) -> None: + await self._a_acquire(key=key or self._key) + + async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + await self._a_release(key=self._key) + + +class SyncLockMethods(LockProtocolBase): + def _acquire(self, key: str) -> None: + cached = self._cachify.get(key=key) + self._check_is_cached(is_already_cached=bool(cached), key=key) + + self._cachify.set(key=key, val=1) + + def _release(self, key: str) -> None: + self._cachify.delete(key=key) + + def __enter__(self) -> None: + self._acquire(key=self._key) + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self._release(key=self._key) + + +class Lock(AsyncLockMethods, SyncLockMethods): + def __init__(self, key: str, reentrant: bool = False, nowait: bool = True, timeout: Optional[int] = None) -> None: + self._key = key + self._reentrant = reentrant + self._nowait = nowait + self._timeout = timeout + + def __call__( + self, func: Union[Callable[P, R], Callable[P, Awaitable[R]]] + ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: + @overload + def _lock_inner( # type: ignore[overload-overlap] + _func: Callable[P, Awaitable[R]], + ) -> AsyncWithResetProtocol[P, R]: ... + + @overload + def _lock_inner( + _func: Callable[P, R], + ) -> SyncWithResetProtocol[P, R]: ... + + def _lock_inner( + _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], + ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: + signature = inspect.signature(_func) + + if is_coroutine(_func): + _awaitable_func = _func + + @wraps(_awaitable_func) + async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + + # try: + # async with self.__aenter__(key=_key): + # return await _awaitable_func(*args, **kwargs) + # except CachifyLockError: + # if raise_on_locked: + # raise + # + # return return_on_locked + + setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) + + return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + + else: + + @wraps(_func) # type: ignore[unreachable] + def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=key) + + # try: + # with lock(key=_key): + # return _func(*args, **kwargs) + # except CachifyLockError: + # if raise_on_locked: + # raise + # + # return return_on_locked + + setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) + + return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + + return _lock_inner + + def _recreate_cm(self): + return self + + @property + def _cachify(self) -> 'Cachify': + return get_cachify() + + @staticmethod + def _check_is_cached(is_already_cached: bool, key: str) -> None: + if not is_already_cached: + return + + logger.warning(msg := f'{key} is already locked!') + raise CachifyLockError(msg) + + @asynccontextmanager async def async_lock(key: str) -> AsyncGenerator[None, None]: _cachify = get_cachify() From 4777e443424f7c555494f02770cb93e8ef158cfd Mon Sep 17 00:00:00 2001 From: Cruitly Date: Fri, 13 Sep 2024 17:31:39 +0300 Subject: [PATCH 2/5] lock refactor --- py_cachify/backend/lock.py | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index e53b5bc..59ac3ae 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -119,16 +119,18 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: bound_args = signature.bind(*args, **kwargs) _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) - # try: - # async with self.__aenter__(key=_key): - # return await _awaitable_func(*args, **kwargs) - # except CachifyLockError: - # if raise_on_locked: - # raise - # - # return return_on_locked - - setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) + try: + async with Lock( + key=_key, + reentrant=self._reentrant, + nowait=self._nowait, + timeout=self._timeout, + ): + return await _awaitable_func(*args, **kwargs) + except CachifyLockError: + pass + + #setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) return cast(AsyncWithResetProtocol[P, R], _async_wrapper) @@ -137,18 +139,15 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: @wraps(_func) # type: ignore[unreachable] def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: bound_args = signature.bind(*args, **kwargs) - _key = get_full_key_from_signature(bound_args=bound_args, key=key) - - # try: - # with lock(key=_key): - # return _func(*args, **kwargs) - # except CachifyLockError: - # if raise_on_locked: - # raise - # - # return return_on_locked - - setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + + try: + with Lock(key=_key, reentrant=self._reentrant, nowait=self._nowait, timeout=self._timeout): + return _func(*args, **kwargs) + except CachifyLockError: + pass + + #setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) return cast(SyncWithResetProtocol[P, R], _sync_wrapper) From ccbac09d72c2885c6512ed07600688bee20789e9 Mon Sep 17 00:00:00 2001 From: galtozzy Date: Thu, 19 Sep 2024 23:31:39 +0200 Subject: [PATCH 3/5] Locks refactor Added docstrings --- .gitignore | 2 + py_cachify/__init__.py | 6 +- py_cachify/asyncio.py | 4 +- py_cachify/backend/cached.py | 36 +++-- py_cachify/backend/helpers.py | 14 ++ py_cachify/backend/lib.py | 22 ++- py_cachify/backend/lock.py | 286 ++++++++++++++++++++-------------- py_cachify/backend/types.py | 32 +++- pyproject.toml | 20 +-- 9 files changed, 274 insertions(+), 148 deletions(-) diff --git a/.gitignore b/.gitignore index 09f41ca..633f578 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,8 @@ wheels/ *.egg-info/ .installed.cfg *.egg +pyrightconfig.json +.zed # PyInstaller # Usually these files are written by a python script from a template diff --git a/py_cachify/__init__.py b/py_cachify/__init__.py index 8c6d808..340f182 100644 --- a/py_cachify/__init__.py +++ b/py_cachify/__init__.py @@ -1,14 +1,16 @@ +from . import asyncio, sync from .backend.cached import cached from .backend.exceptions import CachifyInitError, CachifyLockError -from .backend.helpers import Decoder, Encoder from .backend.lib import init_cachify -from .backend.lock import once +from .backend.lock import lock, once +from .backend.types import Decoder, Encoder __all__ = [ 'CachifyInitError', 'CachifyLockError', 'init_cachify', + 'lock', 'Encoder', 'Decoder', 'cached', diff --git a/py_cachify/asyncio.py b/py_cachify/asyncio.py index fb3a1bc..1b70b54 100644 --- a/py_cachify/asyncio.py +++ b/py_cachify/asyncio.py @@ -1,6 +1,6 @@ from .backend.cached import async_cached -from .backend.lock import async_lock, async_once +from .backend.lock import async_once from .backend.types import AsyncClient -__all__ = ['async_once', 'async_lock', 'async_cached', 'AsyncClient'] +__all__ = ['async_once', 'async_cached', 'AsyncClient'] diff --git a/py_cachify/backend/cached.py b/py_cachify/backend/cached.py index d0e7ca3..e1b3fdc 100644 --- a/py_cachify/backend/cached.py +++ b/py_cachify/backend/cached.py @@ -6,27 +6,45 @@ from .helpers import a_reset, encode_decode_value, get_full_key_from_signature, is_coroutine, reset from .lib import get_cachify -from .types import AsyncWithResetProtocol, Decoder, Encoder, SyncOrAsync, SyncWithResetProtocol +from .types import AsyncWithResetProto, Decoder, Encoder, SyncOrAsyncReset, SyncWithResetProto R = TypeVar('R') P = ParamSpec('P') -def cached(key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None) -> SyncOrAsync: +def cached( + key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None +) -> SyncOrAsyncReset: + """ + Decorator that caches the result of a function based on the specified key, time-to-live (ttl), + and encoding/decoding functions. + + Args: + key (str): The key used to identify the cached result, could be a format string. + ttl (Union[int, None], optional): The time-to-live for the cached result. + Defaults to None, means indefinitely. + enc_dec (Union[Tuple[Encoder, Decoder], None], optional): The encoding and decoding functions for the cached value. + Defaults to None. + + Returns: + WrappedFunction: Either a synchronous or asynchronous function with reset method attached to it, + reset(*args, **kwargs) matches the type of original function and could be used to reset the cache. + """ + @overload def _cached_inner( # type: ignore[overload-overlap] _func: Callable[P, Awaitable[R]], - ) -> AsyncWithResetProtocol[P, R]: ... + ) -> AsyncWithResetProto[P, R]: ... @overload def _cached_inner( _func: Callable[P, R], - ) -> SyncWithResetProtocol[P, R]: ... + ) -> SyncWithResetProto[P, R]: ... def _cached_inner( _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], - ) -> Union[AsyncWithResetProtocol[P, R], SyncWithResetProtocol[P, R]]: + ) -> Union[AsyncWithResetProto[P, R], SyncWithResetProto[P, R]]: signature = inspect.signature(_func) enc, dec = None, None @@ -49,7 +67,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) - return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + return cast(AsyncWithResetProto[P, R], _async_wrapper) else: @wraps(_func) # type: ignore[unreachable] @@ -65,7 +83,7 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) - return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + return cast(SyncWithResetProto[P, R], _sync_wrapper) return _cached_inner @@ -73,12 +91,12 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: @deprecated('sync_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') def sync_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None -) -> SyncOrAsync: +) -> SyncOrAsyncReset: return cached(key=key, ttl=ttl, enc_dec=enc_dec) @deprecated('async_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') def async_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None -) -> SyncOrAsync: +) -> SyncOrAsyncReset: return cached(key=key, ttl=ttl, enc_dec=enc_dec) diff --git a/py_cachify/backend/helpers.py b/py_cachify/backend/helpers.py index 39bd908..5a406cb 100644 --- a/py_cachify/backend/helpers.py +++ b/py_cachify/backend/helpers.py @@ -53,3 +53,17 @@ async def a_reset(*args: Any, key: str, signature: inspect.Signature, **kwargs: await cachify.a_delete(key=_key) return None + + +async def is_alocked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: + cachify = get_cachify() + _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) + + return bool(await cachify.get(key=_key)) + + +async def is_locked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: + cachify = get_cachify() + _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) + + return bool(cachify.get(key=_key)) diff --git a/py_cachify/backend/lib.py b/py_cachify/backend/lib.py index d1640d3..3669b95 100644 --- a/py_cachify/backend/lib.py +++ b/py_cachify/backend/lib.py @@ -11,11 +11,13 @@ def __init__( self, sync_client: Union[SyncClient, MemoryCache], async_client: Union[AsyncClient, AsyncWrapper], + default_expiration: Optional[int], prefix: str, ) -> None: self._sync_client = sync_client self._async_client = async_client self._prefix = prefix + self.default_expiration = default_expiration def set(self, key: str, val: Any, ttl: Union[int, None] = None) -> Any: self._sync_client.set(name=f'{self._prefix}{key}', value=pickle.dumps(val), ex=ttl) @@ -42,10 +44,26 @@ async def a_delete(self, key: str) -> Any: def init_cachify( sync_client: SyncClient = (mc := MemoryCache()), async_client: AsyncClient = AsyncWrapper(cache=mc), - prefix: str = '_PYC_', + default_lock_expiration: Optional[int] = 30, + prefix: str = 'PYC-', ) -> None: + """ + Initialize the Cachify instance with the specified clients and settings. + + Args: + sync_client (Union[SyncClient, MemoryCache], optional): The synchronous client to use. + Defaults to MemoryCache(). + async_client (Union[AsyncClient, AsyncWrapper], optional): The asynchronous client to use. + Defaults to AsyncWrapper(cache=MemoryCache()). + default_lock_expiration (Optional[int], optional): The default expiration time for locks. + Defaults to 30. + prefix (str, optional): The prefix to use for keys. Defaults to 'PYC-'. + """ + global _cachify - _cachify = Cachify(sync_client=sync_client, async_client=async_client, prefix=prefix) + _cachify = Cachify( + sync_client=sync_client, async_client=async_client, prefix=prefix, default_expiration=default_lock_expiration + ) def get_cachify() -> Cachify: diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index 59ac3ae..ef76cce 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -1,27 +1,24 @@ import inspect import logging -from contextlib import asynccontextmanager, contextmanager +import time +from asyncio import sleep as asleep from functools import partial, wraps -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Awaitable, - Callable, - Generator, - Optional, - Protocol, - TypeVar, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union, cast -from typing_extensions import ParamSpec, deprecated, overload +from typing_extensions import ParamSpec, Self, deprecated, overload from .exceptions import CachifyLockError -from .helpers import a_reset, get_full_key_from_signature, is_coroutine, reset +from .helpers import a_reset, get_full_key_from_signature, is_alocked, is_coroutine, is_locked, reset from .lib import get_cachify -from .types import AsyncWithResetProtocol, SyncOrAsync, SyncWithResetProtocol +from .types import ( + UNSET, + AsyncLockedProto, + AsyncWithResetProto, + SyncLockedProto, + SyncOrAsyncReset, + SyncWithResetProto, + UnsetType, +) if TYPE_CHECKING: @@ -33,182 +30,241 @@ P = ParamSpec('P') -def _check_is_cached(is_already_cached: bool, key: str) -> None: +def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: if not is_already_cached: - return + return True logger.warning(msg := f'{key} is already locked!') - raise CachifyLockError(msg) + if do_raise: + raise CachifyLockError(msg) + + return False class LockProtocolBase(Protocol): - _cachify: 'Cachify' _key: str - _reentrant: bool _nowait: bool - _timeout: Optional[int] + _timeout: Optional[Union[int, float]] + _exp: Union[Optional[int], UnsetType] @staticmethod - def _check_is_cached(is_already_cached: bool, key: str) -> None: ... + def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: ... + + @property + def _cachify(self) -> 'Cachify': ... + + def _calc_stop_at(self) -> float: ... + + def _get_ttl(self) -> Optional[int]: ... class AsyncLockMethods(LockProtocolBase): + async def is_alocked(self) -> bool: + return bool(await self._cachify.get(key=self._key)) + async def _a_acquire(self, key: str) -> None: - cached = await self._cachify.a_get(key=key) - self._check_is_cached(is_already_cached=bool(cached), key=key) + stop_at = self._calc_stop_at() - await self._cachify.a_set(key=key, val=1) + while True: + _is_locked = self._check_is_cached( + is_already_cached=await self.is_alocked(), + key=key, + do_raise=self._nowait or time.time() > stop_at, + ) + + if not _is_locked: + await self._cachify.a_set(key=key, val=1, ttl=self._get_ttl()) + return + + await asleep(0.1) async def _a_release(self, key: str) -> None: await self._cachify.a_delete(key=key) - async def __aenter__(self, key: Optional[str] = None) -> None: + async def __aenter__(self, key: Optional[str] = None) -> 'Self': await self._a_acquire(key=key or self._key) + return self async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: await self._a_release(key=self._key) class SyncLockMethods(LockProtocolBase): + def is_locked(self) -> bool: + return bool(self._cachify.get(key=self._key)) + def _acquire(self, key: str) -> None: - cached = self._cachify.get(key=key) - self._check_is_cached(is_already_cached=bool(cached), key=key) + stop_at = self._calc_stop_at() + + while True: + _is_locked = self._check_is_cached( + is_already_cached=bool(self.is_locked), + key=key, + do_raise=self._nowait or time.time() > stop_at, + ) + + if not _is_locked: + self._cachify.set(key=key, val=1, ttl=self._get_ttl()) + return - self._cachify.set(key=key, val=1) + time.sleep(0.1) def _release(self, key: str) -> None: self._cachify.delete(key=key) - def __enter__(self) -> None: + def __enter__(self) -> 'Self': self._acquire(key=self._key) + return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self._release(key=self._key) -class Lock(AsyncLockMethods, SyncLockMethods): - def __init__(self, key: str, reentrant: bool = False, nowait: bool = True, timeout: Optional[int] = None) -> None: +class _Lock(AsyncLockMethods, SyncLockMethods): + """ + Class to manage locking mechanism for synchronous and asynchronous functions. + + Args: + key (str): The key used to identify the lock. + nowait (bool, optional): If True, do not wait for the lock to be released. Defaults to True. + timeout (Union[int, float], optional): The time in seconds to wait for the lock if nowait is False. + Defaults to None. + exp (Union[int, None], optional): The expiration time for the lock. + Defaults to UNSET and global value from cachify is used in that case. + + Methods: + __enter__: Acquire a lock for the specified key, synchronous. + is_locked: Check if the lock is currently held, synchronous. + + + __aenter__: Async version of __enter__ to acquire a lock for the specified key. + is_alocked: Check if the lock is currently held asynchronously. + + __call__: Decorator to acquire a lock for the wrapped function and handle synchronization + for synchronous and asynchronous functions. + Attaches method `is_locked(*args, **kwargs)` to a wrapped function to quickly check if it's locked. + """ + + def __init__( + self, + key: str, + nowait: bool = True, + timeout: Optional[Union[int, float]] = None, + exp: Union[Optional[int], UnsetType] = UNSET, + ) -> None: self._key = key - self._reentrant = reentrant self._nowait = nowait self._timeout = timeout + self._exp = exp - def __call__( - self, func: Union[Callable[P, R], Callable[P, Awaitable[R]]] - ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: - @overload - def _lock_inner( # type: ignore[overload-overlap] - _func: Callable[P, Awaitable[R]], - ) -> AsyncWithResetProtocol[P, R]: ... - - @overload - def _lock_inner( - _func: Callable[P, R], - ) -> SyncWithResetProtocol[P, R]: ... - - def _lock_inner( - _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], - ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: - signature = inspect.signature(_func) + @overload + def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncLockedProto[P, R]: ... - if is_coroutine(_func): - _awaitable_func = _func + @overload + def __call__(self, _func: Callable[P, R]) -> SyncLockedProto[P, R]: ... - @wraps(_awaitable_func) - async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: - bound_args = signature.bind(*args, **kwargs) - _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + def __call__( # type: ignore[misc] + self, _func: Union[Callable[P, Awaitable[R]], Callable[P, R]] + ) -> Union[AsyncLockedProto[P, R], SyncLockedProto[P, R]]: + signature = inspect.signature(_func) - try: - async with Lock( - key=_key, - reentrant=self._reentrant, - nowait=self._nowait, - timeout=self._timeout, - ): - return await _awaitable_func(*args, **kwargs) - except CachifyLockError: - pass + if is_coroutine(_func): + _awaitable_func = _func - #setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) + @wraps(_awaitable_func) + async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) - return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + try: + async with lock( + key=_key, + nowait=self._nowait, + timeout=self._timeout, + exp=self._exp, + ): + return await _awaitable_func(*args, **kwargs) + except CachifyLockError: + raise - else: + setattr(_async_wrapper, 'is_locked', partial(is_alocked, signature=signature, key=self._key)) - @wraps(_func) # type: ignore[unreachable] - def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: - bound_args = signature.bind(*args, **kwargs) - _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + return cast(AsyncLockedProto[P, R], _async_wrapper) - try: - with Lock(key=_key, reentrant=self._reentrant, nowait=self._nowait, timeout=self._timeout): - return _func(*args, **kwargs) - except CachifyLockError: - pass + else: - #setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) + @wraps(_func) # type: ignore[unreachable] + def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) - return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + try: + with lock(key=_key, nowait=self._nowait, timeout=self._timeout, exp=self._exp): + return _func(*args, **kwargs) + except CachifyLockError: + raise - return _lock_inner + setattr(_sync_wrapper, 'is_locked', partial(is_locked, signature=signature, key=self._key)) - def _recreate_cm(self): - return self + return cast(SyncLockedProto[P, R], cast(object, _sync_wrapper)) @property def _cachify(self) -> 'Cachify': return get_cachify() + def _recreate_cm(self) -> 'Self': + return self + + def _calc_stop_at(self) -> float: + return time.time() + self._timeout if self._timeout is not None else float('inf') + + def _get_ttl(self) -> Optional[int]: + return cast(Optional[int], self._exp or self._cachify.default_expiration) + @staticmethod - def _check_is_cached(is_already_cached: bool, key: str) -> None: + def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: if not is_already_cached: - return + return True logger.warning(msg := f'{key} is already locked!') - raise CachifyLockError(msg) + if do_raise: + raise CachifyLockError(msg) + return False -@asynccontextmanager -async def async_lock(key: str) -> AsyncGenerator[None, None]: - _cachify = get_cachify() - cached = await _cachify.a_get(key=key) - _check_is_cached(is_already_cached=bool(cached), key=key) - await _cachify.a_set(key=key, val=1) - try: - yield - finally: - await _cachify.a_delete(key=key) +lock = _Lock -@contextmanager -def lock(key: str) -> Generator[None, None, None]: - _cachify = get_cachify() - cached = _cachify.get(key=key) - _check_is_cached(is_already_cached=bool(cached), key=key) +def once(key: str, raise_on_locked: bool = False, return_on_locked: Any = None) -> SyncOrAsyncReset: + """ + Decorator that ensures a function is only called once at a time, + based on a specified key (could be a format string). - _cachify.set(key=key, val=1) - try: - yield - finally: - _cachify.delete(key=key) + Args: + key (str): The key used to identify the lock. + raise_on_locked (bool, optional): If True, raise an exception when the function is already locked. + Defaults to False. + return_on_locked (Any, optional): The value to return when the function is already locked. + Defaults to None. + Returns: + SyncOrAsyncReset: Either a synchronous or asynchronous wrapped function with reset method attached to it. + """ -def once(key: str, raise_on_locked: bool = False, return_on_locked: Any = None) -> SyncOrAsync: @overload def _once_inner( # type: ignore[overload-overlap] _func: Callable[P, Awaitable[R]], - ) -> AsyncWithResetProtocol[P, R]: ... + ) -> AsyncWithResetProto[P, R]: ... @overload def _once_inner( _func: Callable[P, R], - ) -> SyncWithResetProtocol[P, R]: ... + ) -> SyncWithResetProto[P, R]: ... def _once_inner( _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], - ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: + ) -> Union[SyncWithResetProto[P, R], AsyncWithResetProto[P, R]]: signature = inspect.signature(_func) if is_coroutine(_func): @@ -220,7 +276,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: _key = get_full_key_from_signature(bound_args=bound_args, key=key) try: - async with async_lock(key=_key): + async with lock(key=_key): return await _awaitable_func(*args, **kwargs) except CachifyLockError: if raise_on_locked: @@ -230,7 +286,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) - return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + return cast(AsyncWithResetProto[P, R], _async_wrapper) else: @@ -250,7 +306,7 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) - return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + return cast(SyncWithResetProto[P, R], _sync_wrapper) return _once_inner diff --git a/py_cachify/backend/types.py b/py_cachify/backend/types.py index 03c4aa3..2880230 100644 --- a/py_cachify/backend/types.py +++ b/py_cachify/backend/types.py @@ -32,25 +32,45 @@ def set(self, name: str, value: Any, ex: Union[int, None] = None) -> Any: raise NotImplementedError -class AsyncWithResetProtocol(Protocol[P, R]): +class SyncLockedProto(Protocol[P, R]): + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover + + def is_locked(self, *args: P.args, **kwargs: P.kwargs) -> bool: ... # pragma: no cover + + +class AsyncLockedProto(Protocol[P, R]): + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover + + def is_locked(self, *args: P.args, **kwargs: P.kwargs) -> bool: ... # pragma: no cover + + +class AsyncWithResetProto(Protocol[P, R]): async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover async def reset(self, *args: P.args, **kwargs: P.kwargs) -> None: ... # pragma: no cover -class SyncWithResetProtocol(Protocol[P, R]): +class SyncWithResetProto(Protocol[P, R]): def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover def reset(self, *args: P.args, **kwargs: P.kwargs) -> None: ... # pragma: no cover -class SyncOrAsync(Protocol): +class SyncOrAsyncReset(Protocol): @overload - def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncWithResetProtocol[P, R]: ... # type: ignore[overload-overlap] + def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncWithResetProto[P, R]: ... # type: ignore[overload-overlap] @overload - def __call__(self, _func: Callable[P, R]) -> SyncWithResetProtocol[P, R]: ... + def __call__(self, _func: Callable[P, R]) -> SyncWithResetProto[P, R]: ... def __call__( self, _func: Union[Callable[P, Awaitable[R]], Callable[P, R]] - ) -> Union[AsyncWithResetProtocol[P, R], SyncWithResetProtocol[P, R]]: ... + ) -> Union[AsyncWithResetProto[P, R], SyncWithResetProto[P, R]]: ... + + +class UnsetType: + def __bool__(self) -> bool: + return False + + +UNSET = UnsetType() diff --git a/pyproject.toml b/pyproject.toml index a1a837b..24d1046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Distributed locks and caching/locking decorators at hand" authors = ["Galtozzy "] readme = "README.md" include = ["README.md"] -packages = [{include = 'py_cachify'}] +packages = [{ include = 'py_cachify' }] [project] requires-python = ">=3.8,<4.0" @@ -66,12 +66,12 @@ select = [ "I", # isort "C", # flake8-comprehensions "B", # flake8-bugbear - "UP", # pyupgrade + "UP", # pyupgrade ] ignore = [ - "B008", # do not perform function calls in argument defaults - "C901", # too complex - "B010", # do not rewrite setattr() + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "B010", # do not rewrite setattr() ] [tool.ruff.format] @@ -87,7 +87,7 @@ max-complexity = 6 [tool.ruff.lint.isort] split-on-trailing-comma = false lines-after-imports = 2 -known-first-party = ["py_cachify",] +known-first-party = ["py_cachify"] [tool.ruff.lint.pyupgrade] keep-runtime-typing = true @@ -120,9 +120,7 @@ follow_imports_for_stubs = true show_error_codes = true plugins = [] -exclude = [ - 'test_', -] +exclude = ['test_'] [[tool.mypy.overrides]] module = "tests.*" @@ -147,9 +145,7 @@ asyncio_mode = "auto" python_files = "test*.py" # Directories that are not visited by pytest collector: norecursedirs = "*.egg .eggs dist build docs .tox .git __pycache__ config docker etc" -testpaths = [ - "tests", -] +testpaths = ["tests"] [tool.coverage.run] # Coverage configuration: From 6701efd1c4bfe62a2221059ecc0a65865befdb3e Mon Sep 17 00:00:00 2001 From: galtozzy Date: Sun, 22 Sep 2024 15:57:42 +0200 Subject: [PATCH 4/5] Locks reimplemented & enchanced --- poetry.lock | 367 ++++++++++++++++++---------------- py_cachify/__init__.py | 2 + py_cachify/backend/helpers.py | 4 +- py_cachify/backend/lock.py | 85 +++----- py_cachify/backend/types.py | 23 ++- pyproject.toml | 3 +- tests/conftest.py | 1 + tests/test_backend.py | 2 +- tests/test_cached.py | 6 +- tests/test_helpers.py | 25 ++- tests/test_lock_decorator.py | 194 ++++++++++++++++++ tests/test_locks.py | 155 +++++++++++++- tests/test_once_decorator.py | 6 +- 13 files changed, 628 insertions(+), 245 deletions(-) create mode 100644 tests/test_lock_decorator.py diff --git a/poetry.lock b/poetry.lock index acc99bf..ec300fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -30,13 +30,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -283,33 +283,40 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -439,13 +446,13 @@ files = [ [[package]] name = "mkdocs" -version = "1.6.0" +version = "1.6.1" description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, - {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, ] [package.dependencies] @@ -487,13 +494,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.33" +version = "9.5.36" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.33-py3-none-any.whl", hash = "sha256:dbc79cf0fdc6e2c366aa987de8b0c9d4e2bb9f156e7466786ba2fd0f9bf7ffca"}, - {file = "mkdocs_material-9.5.33.tar.gz", hash = "sha256:d23a8b5e3243c9b2f29cdfe83051104a8024b767312dc8fde05ebe91ad55d89d"}, + {file = "mkdocs_material-9.5.36-py3-none-any.whl", hash = "sha256:36734c1fd9404bea74236242ba3359b267fc930c7233b9fd086b0898825d0ac9"}, + {file = "mkdocs_material-9.5.36.tar.gz", hash = "sha256:140456f761320f72b399effc073fa3f8aac744c77b0970797c201cae2f6c967f"}, ] [package.dependencies] @@ -538,38 +545,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, - {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, - {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, - {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, - {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, - {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, - {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] @@ -607,14 +614,19 @@ files = [ [[package]] name = "paginate" -version = "0.5.6" +version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, ] +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pathspec" version = "0.12.1" @@ -628,19 +640,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -701,13 +713,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.9" +version = "10.10" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, - {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, + {file = "pymdown_extensions-10.10-py3-none-any.whl", hash = "sha256:5593c49032dc674c30c8382591324de92049520e6c3dd332bfba7f696d55ec34"}, + {file = "pymdown_extensions-10.10.tar.gz", hash = "sha256:13899c05c2d3cd486e9f57963f7154d2fed2c57530c50a7774fc13041610e82a"}, ] [package.dependencies] @@ -719,13 +731,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -822,13 +834,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] @@ -927,90 +939,105 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "regex" -version = "2024.7.24" +version = "2024.9.11" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, - {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, - {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, - {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, - {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, - {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, - {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, - {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, - {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, - {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, - {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, - {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, + {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, + {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, + {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, + {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, + {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, + {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, + {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, + {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, + {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, + {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, + {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, + {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, + {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, ] [[package]] @@ -1036,29 +1063,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.6.2" +version = "0.6.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, + {file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"}, + {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"}, + {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"}, + {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"}, + {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"}, + {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"}, + {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"}, ] [[package]] @@ -1113,13 +1140,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -1177,18 +1204,22 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "zipp" -version = "3.20.0" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" diff --git a/py_cachify/__init__.py b/py_cachify/__init__.py index 340f182..a2c776b 100644 --- a/py_cachify/__init__.py +++ b/py_cachify/__init__.py @@ -6,6 +6,8 @@ from .backend.types import Decoder, Encoder +__version__ = '1.2.0' + __all__ = [ 'CachifyInitError', 'CachifyLockError', diff --git a/py_cachify/backend/helpers.py b/py_cachify/backend/helpers.py index 5a406cb..48fd84a 100644 --- a/py_cachify/backend/helpers.py +++ b/py_cachify/backend/helpers.py @@ -59,10 +59,10 @@ async def is_alocked(*args: Any, key: str, signature: inspect.Signature, **kwarg cachify = get_cachify() _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) - return bool(await cachify.get(key=_key)) + return bool(await cachify.a_get(key=_key)) -async def is_locked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: +def is_locked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: cachify = get_cachify() _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index ef76cce..806d4b3 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -3,7 +3,7 @@ import time from asyncio import sleep as asleep from functools import partial, wraps -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, TypeVar, Union, cast from typing_extensions import ParamSpec, Self, deprecated, overload @@ -14,6 +14,7 @@ UNSET, AsyncLockedProto, AsyncWithResetProto, + LockProtocolBase, SyncLockedProto, SyncOrAsyncReset, SyncWithResetProto, @@ -30,44 +31,17 @@ P = ParamSpec('P') -def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: - if not is_already_cached: - return True - - logger.warning(msg := f'{key} is already locked!') - if do_raise: - raise CachifyLockError(msg) - - return False - - -class LockProtocolBase(Protocol): - _key: str - _nowait: bool - _timeout: Optional[Union[int, float]] - _exp: Union[Optional[int], UnsetType] - - @staticmethod - def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: ... - - @property - def _cachify(self) -> 'Cachify': ... - - def _calc_stop_at(self) -> float: ... - - def _get_ttl(self) -> Optional[int]: ... - - class AsyncLockMethods(LockProtocolBase): async def is_alocked(self) -> bool: - return bool(await self._cachify.get(key=self._key)) + return bool(await self._cachify.a_get(key=self._key)) async def _a_acquire(self, key: str) -> None: stop_at = self._calc_stop_at() while True: - _is_locked = self._check_is_cached( - is_already_cached=await self.is_alocked(), + _is_locked = bool(await self.is_alocked()) + self._raise_if_cached( + is_already_cached=_is_locked, key=key, do_raise=self._nowait or time.time() > stop_at, ) @@ -81,8 +55,8 @@ async def _a_acquire(self, key: str) -> None: async def _a_release(self, key: str) -> None: await self._cachify.a_delete(key=key) - async def __aenter__(self, key: Optional[str] = None) -> 'Self': - await self._a_acquire(key=key or self._key) + async def __aenter__(self) -> 'Self': + await self._a_acquire(key=self._key) return self async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: @@ -97,8 +71,9 @@ def _acquire(self, key: str) -> None: stop_at = self._calc_stop_at() while True: - _is_locked = self._check_is_cached( - is_already_cached=bool(self.is_locked), + _is_locked = bool(self.is_locked()) + self._raise_if_cached( + is_already_cached=_is_locked, key=key, do_raise=self._nowait or time.time() > stop_at, ) @@ -120,7 +95,7 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self._release(key=self._key) -class _Lock(AsyncLockMethods, SyncLockMethods): +class lock(AsyncLockMethods, SyncLockMethods): """ Class to manage locking mechanism for synchronous and asynchronous functions. @@ -136,7 +111,6 @@ class _Lock(AsyncLockMethods, SyncLockMethods): __enter__: Acquire a lock for the specified key, synchronous. is_locked: Check if the lock is currently held, synchronous. - __aenter__: Async version of __enter__ to acquire a lock for the specified key. is_alocked: Check if the lock is currently held asynchronously. @@ -176,16 +150,13 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: bound_args = signature.bind(*args, **kwargs) _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) - try: - async with lock( - key=_key, - nowait=self._nowait, - timeout=self._timeout, - exp=self._exp, - ): - return await _awaitable_func(*args, **kwargs) - except CachifyLockError: - raise + async with lock( + key=_key, + nowait=self._nowait, + timeout=self._timeout, + exp=self._exp, + ): + return await _awaitable_func(*args, **kwargs) setattr(_async_wrapper, 'is_locked', partial(is_alocked, signature=signature, key=self._key)) @@ -198,11 +169,8 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: bound_args = signature.bind(*args, **kwargs) _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) - try: - with lock(key=_key, nowait=self._nowait, timeout=self._timeout, exp=self._exp): - return _func(*args, **kwargs) - except CachifyLockError: - raise + with lock(key=_key, nowait=self._nowait, timeout=self._timeout, exp=self._exp): + return _func(*args, **kwargs) setattr(_sync_wrapper, 'is_locked', partial(is_locked, signature=signature, key=self._key)) @@ -219,22 +187,17 @@ def _calc_stop_at(self) -> float: return time.time() + self._timeout if self._timeout is not None else float('inf') def _get_ttl(self) -> Optional[int]: - return cast(Optional[int], self._exp or self._cachify.default_expiration) + return self._cachify.default_expiration if isinstance(self._exp, UnsetType) else self._exp @staticmethod - def _check_is_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> bool: + def _raise_if_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> None: if not is_already_cached: - return True + return logger.warning(msg := f'{key} is already locked!') if do_raise: raise CachifyLockError(msg) - return False - - -lock = _Lock - def once(key: str, raise_on_locked: bool = False, return_on_locked: Any = None) -> SyncOrAsyncReset: """ diff --git a/py_cachify/backend/types.py b/py_cachify/backend/types.py index 2880230..481fcd0 100644 --- a/py_cachify/backend/types.py +++ b/py_cachify/backend/types.py @@ -1,8 +1,12 @@ -from typing import Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union from typing_extensions import ParamSpec, TypeAlias, overload +if TYPE_CHECKING: + from .lib import Cachify + + R = TypeVar('R', covariant=True) P = ParamSpec('P') @@ -74,3 +78,20 @@ def __bool__(self) -> bool: UNSET = UnsetType() + + +class LockProtocolBase(Protocol): + _key: str + _nowait: bool + _timeout: Optional[Union[int, float]] + _exp: Union[Optional[int], UnsetType] + + @staticmethod + def _raise_if_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> None: ... # pragma: no cover + + @property + def _cachify(self) -> 'Cachify': ... # pragma: no cover + + def _calc_stop_at(self) -> float: ... # pragma: no cover + + def _get_ttl(self) -> Optional[int]: ... # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 24d1046..41586db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ redis = "^5.0.7" format-and-lint = "task ruff && task mypy-lint" ruff = "ruff format ./py_cachify/ ./tests/ ./integration_tests/ && ruff check ./py_cachify/ ./tests/ ./integration_tests/ --fix --unsafe-fixes" -tests = "PYTHONPATH=. pytest tests/ -v" +tests = "PYTHONPATH=. pytest tests/ -vvv" integration-tests = "PYTHONPATH=. pytest integration_tests/ --no-cov" ruff-lint = "ruff check ./py_cachify" @@ -166,6 +166,7 @@ exclude_lines = [ 'raise NotImplementedError', 'if __name__ == .__main__.:', '__all__', + 'if TYPE_CHECKING:', ] [build-system] diff --git a/tests/conftest.py b/tests/conftest.py index 7efe247..5b79f97 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,4 +8,5 @@ def init_cachify_fixture(): init_cachify() yield + py_cachify.backend.lib._cachify._sync_client._cache = {} py_cachify.backend.lib._cachify = None diff --git a/tests/test_backend.py b/tests/test_backend.py index b0d5171..28a6f6b 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -21,7 +21,7 @@ def async_wrapper(memory_cache): @pytest.fixture def cachify(memory_cache, async_wrapper): - return Cachify(sync_client=memory_cache, async_client=async_wrapper, prefix='_PYC_') + return Cachify(sync_client=memory_cache, default_expiration=30, async_client=async_wrapper, prefix='_PYC_') def test_memory_cache_set_and_get(memory_cache): diff --git a/tests/test_cached.py b/tests/test_cached.py index 9e39f33..c44d5da 100644 --- a/tests/test_cached.py +++ b/tests/test_cached.py @@ -7,7 +7,7 @@ from py_cachify import cached from py_cachify.backend.exceptions import CachifyInitError -from py_cachify.backend.types import AsyncWithResetProtocol, P, R, SyncWithResetProtocol +from py_cachify.backend.types import AsyncWithResetProto, P, R, SyncWithResetProto def sync_function(arg1: int, arg2: int) -> int: @@ -75,7 +75,7 @@ def test_sync_cached_preserves_type_annotations(init_cachify_fixture): for name, clz in [('arg1', int), ('arg2', int), ('return', int)]: assert func.__annotations__[name] == clz - assert_type(func, SyncWithResetProtocol[P, R]) + assert_type(func, SyncWithResetProto[P, R]) def test_async_cached_preserves_type_annotations(init_cachify_fixture): @@ -83,7 +83,7 @@ def test_async_cached_preserves_type_annotations(init_cachify_fixture): for name, clz in [('arg1', int), ('arg2', int), ('return', int)]: assert func.__annotations__[name] == clz - assert_type(func, AsyncWithResetProtocol[P, R]) + assert_type(func, AsyncWithResetProto[P, R]) def test_cached_wrapped_async_function_has_reset_callable_attached(init_cachify_fixture): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 915fe15..c8f3910 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -3,7 +3,7 @@ import pytest from pytest_mock import MockerFixture -from py_cachify.backend.helpers import a_reset, get_full_key_from_signature, reset +from py_cachify.backend.helpers import a_reset, get_full_key_from_signature, is_alocked, is_locked, reset def method_with_args_kwargs_args(*args, **kwargs) -> None: @@ -54,3 +54,26 @@ async def test_a_reset_calls_delete_with_key(init_cachify_fixture, args_kwargs_s await a_reset('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) mock.assert_called_once_with(key='key_val1_val2_val3') + + +@pytest.mark.asyncio +@pytest.mark.parametrize('val', [0, 1]) +async def test_is_alocked_accesses_a_get_with_key( + init_cachify_fixture, args_kwargs_signature, mocker: MockerFixture, val +): + mock = mocker.patch('py_cachify.backend.lib.Cachify.a_get', return_value=val) + + res = await is_alocked('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) + + mock.assert_called_once_with(key='key_val1_val2_val3') + assert res is bool(val) + + +@pytest.mark.parametrize('val', [0, 1]) +def test_is_locked_accesses_get_with_key(init_cachify_fixture, args_kwargs_signature, mocker: MockerFixture, val): + mock = mocker.patch('py_cachify.backend.lib.Cachify.get', return_value=val) + + res = is_locked('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) + + mock.assert_called_once_with(key='key_val1_val2_val3') + assert res is bool(val) diff --git a/tests/test_lock_decorator.py b/tests/test_lock_decorator.py new file mode 100644 index 0000000..888aa57 --- /dev/null +++ b/tests/test_lock_decorator.py @@ -0,0 +1,194 @@ +import asyncio +from asyncio import sleep as asleep +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext +from time import sleep + +import pytest + +from py_cachify.backend.exceptions import CachifyLockError +from py_cachify.backend.lib import init_cachify +from py_cachify.backend.lock import lock +from py_cachify.backend.types import UNSET + + +@pytest.mark.parametrize( + 'sleep_time,input1,input2,result1,result2', + [ + (1, 3, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (1, 3, 5, nullcontext(13), nullcontext(15)), + ], +) +def test_lock_decorator_no_wait_sync(init_cachify_fixture, sleep_time, input1, input2, result1, result2): + @lock(key='test_key-{arg}') + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=input1), + e.submit(sync_function, arg=input2), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,input1,input2,result1,result2', + [ + (1, 3, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (1, 3, 5, nullcontext(13), nullcontext(15)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_no_wait_async(init_cachify_fixture, sleep_time, input1, input2, result1, result2): + @lock(key='test_key-{arg}') + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(input1)) + task2 = asyncio.create_task(async_function(input2)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +@pytest.mark.parametrize( + 'sleep_time,timeout,input,result1,result2', + [ + (1, 2, 3, nullcontext(13), nullcontext(13)), + (2, 1, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (0, 1, 3, nullcontext(13), nullcontext(13)), + ], +) +def test_lock_decorator_no_wait_false_sync(init_cachify_fixture, sleep_time, timeout, input, result1, result2): + @lock(key='test_key-{arg}', nowait=False, timeout=timeout) + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=input), + e.submit(sync_function, arg=input), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,timeout,input,result1,result2', + [ + (1, 2, 3, nullcontext(13), nullcontext(13)), + (1, 0.5, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (0, 0.5, 3, nullcontext(13), nullcontext(13)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_no_wait_false_async(init_cachify_fixture, sleep_time, timeout, input, result1, result2): + @lock(key='test_key-{arg}', nowait=False, timeout=timeout) + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(input)) + task2 = asyncio.create_task(async_function(input)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +@pytest.mark.parametrize( + 'sleep_time,timeout,exp,default_exp,result1,result2', + [ + (3, 2, 1, None, nullcontext(15), nullcontext(15)), + (2, 2, UNSET, 1, nullcontext(15), nullcontext(15)), + (2, 1, UNSET, 2, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 4, 1, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 1, 4, nullcontext(15), nullcontext(15)), + ], +) +def test_lock_decorator_expiration_sync(init_cachify_fixture, sleep_time, timeout, exp, default_exp, result1, result2): + init_cachify(default_lock_expiration=default_exp) + + @lock(key='test_key-{arg}', nowait=False, timeout=timeout, exp=exp) + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=5), + e.submit(sync_function, arg=5), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,timeout,exp,default_exp,result1,result2', + [ + (3, 2, 1, None, nullcontext(15), nullcontext(15)), + (2, 2, UNSET, 1, nullcontext(15), nullcontext(15)), + (2, 1, UNSET, 2, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 4, 1, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 1, 4, nullcontext(15), nullcontext(15)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_expiration_async( + init_cachify_fixture, sleep_time, timeout, exp, default_exp, result1, result2 +): + init_cachify(default_lock_expiration=default_exp) + + @lock(key='test_key-{arg}', nowait=False, timeout=timeout, exp=exp) + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(5)) + task2 = asyncio.create_task(async_function(5)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +def test_wrapped_function_has_is_locked_sync(): + @lock(key='test_key') + def sync_function(): ... + + assert hasattr(sync_function, 'is_locked') + assert not asyncio.iscoroutinefunction(sync_function.is_locked) + assert callable(sync_function.is_locked) + + +def test_wrapped_function_has_is_locked_async(): + @lock(key='test_key') + async def async_function(): ... + + assert hasattr(async_function, 'is_locked') + assert asyncio.iscoroutinefunction(async_function.is_locked) + assert callable(async_function.is_locked) diff --git a/tests/test_locks.py b/tests/test_locks.py index b757c78..f45cae8 100644 --- a/tests/test_locks.py +++ b/tests/test_locks.py @@ -1,13 +1,24 @@ +import asyncio +from asyncio import sleep as asleep +from contextlib import nullcontext +from threading import Thread +from time import sleep + import pytest from py_cachify import CachifyLockError -from py_cachify.backend.lock import async_lock, lock +from py_cachify.backend.lib import Cachify, init_cachify +from py_cachify.backend.lock import lock +from py_cachify.backend.types import UNSET + + +lock_obj = lock(key='test') @pytest.mark.asyncio async def test_async_lock(init_cachify_fixture): async def async_operation(): - async with async_lock('lock'): + async with lock('lock'): return None await async_operation() @@ -18,8 +29,8 @@ async def test_async_lock_already_locked(init_cachify_fixture): key = 'lock' async def async_operation(): - async with async_lock(key): - async with async_lock(key): + async with lock(key): + async with lock(key): pass with pytest.raises(CachifyLockError, match=f'{key} is already locked!'): @@ -44,3 +55,139 @@ def sync_operation(): with pytest.raises(CachifyLockError, match=f'{key} is already locked!'): sync_operation() + + +@pytest.mark.parametrize( + 'exp,timeout,expectation', [(1, 2, nullcontext(None)), (2, 1, pytest.raises(CachifyLockError))] +) +def test_waiting_lock(init_cachify_fixture, exp, timeout, expectation): + key = 'lock' + + def sync_operation(): + with lock(key=key, exp=exp): + with lock(key=key, nowait=False, timeout=timeout): + return None + + with expectation as e: + assert sync_operation() == e + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'exp,timeout,expectation', [(1, 2, nullcontext(None)), (2, 1, pytest.raises(CachifyLockError))] +) +async def test_waiting_lock_async(init_cachify_fixture, exp, timeout, expectation): + key = 'lock' + + async def async_operation(): + async with lock(key=key, exp=exp): + async with lock(key=key, nowait=False, timeout=timeout): + return None + + with expectation as e: + assert await async_operation() == e + + +def test_lock_cachify_returns_cachify_instance(init_cachify_fixture): + assert isinstance(lock_obj._cachify, Cachify) + assert lock_obj._cachify is not None + + +def test_lock_recreate_cm_returns_self(): + assert lock_obj._recreate_cm() is lock_obj + + +@pytest.mark.parametrize('timeout,expected', [(None, float('inf')), (10, 20.0)]) +def test_lock_calc_stop_at(mocker, timeout, expected): + new_lock = lock('test', timeout=timeout) + mocker.patch('time.time', return_value=10.0) + + assert new_lock._calc_stop_at() == expected + + +@pytest.mark.parametrize( + 'default_expiration,exp,expected', + [ + (None, UNSET, 30), + (60, UNSET, 60), + (30, 60, 60), + (30, None, None), + ], +) +def test_lock_get_ttl(init_cachify_fixture, default_expiration, exp, expected): + init_dict = {'default_lock_expiration': default_expiration} if default_expiration is not None else {} + + init_cachify(**init_dict) + + lock_obj = lock('test', exp=exp) + + assert lock_obj._get_ttl() == expected + + +@pytest.mark.parametrize( + 'is_already_locked,key,do_raise,expectation', + [ + (True, 'test', False, nullcontext(None)), + (True, 'test', True, pytest.raises(CachifyLockError)), + (False, 'test', True, nullcontext(None)), + ], +) +def test_lock_raise_if_cached(mocker, is_already_locked, key, do_raise, expectation): + patch_log = mocker.patch('py_cachify.backend.lock.logger.warning') + + with expectation: + lock._raise_if_cached( + is_already_cached=is_already_locked, + key=key, + do_raise=do_raise, + ) + if is_already_locked is True: + patch_log.assert_called_once_with(f'{key} is already locked!') + + +def test_unset_type_bool(): + assert bool(UNSET) is False + + +@pytest.mark.parametrize( + 'sleep_time,expected', + [ + (3, True), + (0, False), + ], +) +def test_is_locked_on_lock_obj(init_cachify_fixture, sleep_time, expected): + test_lock = lock('test') + + def sync_function(): + with test_lock: + sleep(sleep_time) + + thread = Thread(target=sync_function) + thread.start() + sleep(0.3) + + assert test_lock.is_locked() is expected + + +@pytest.mark.parametrize( + 'sleep_time,expected', + [ + (3, True), + (0, False), + ], +) +async def test_is_locked_on_lock_obj_async(init_cachify_fixture, sleep_time, expected): + test_lock = lock('test') + + async def async_function(): + async with test_lock: + await asleep(sleep_time) + + task = asyncio.create_task(async_function()) + + await asleep(0.2) + + assert await test_lock.is_alocked() is expected + + await task diff --git a/tests/test_once_decorator.py b/tests/test_once_decorator.py index c031623..e2d2061 100644 --- a/tests/test_once_decorator.py +++ b/tests/test_once_decorator.py @@ -7,7 +7,7 @@ from py_cachify import CachifyLockError, once from py_cachify.backend.lock import async_once, sync_once -from py_cachify.backend.types import AsyncWithResetProtocol, P, R, SyncWithResetProtocol +from py_cachify.backend.types import AsyncWithResetProto, P, R, SyncWithResetProto def test_once_decorator_sync_function(init_cachify_fixture): @@ -105,8 +105,8 @@ def sync_function(arg1: int, arg2: int) -> int: assert sync_function.__annotations__[name] == clz assert async_function.__annotations__[name] == clz - assert_type(sync_function, SyncWithResetProtocol[P, R]) - assert_type(async_function, AsyncWithResetProtocol[P, R]) + assert_type(sync_function, SyncWithResetProto[P, R]) + assert_type(async_function, AsyncWithResetProto[P, R]) def test_once_wrapped_async_function_has_reset_callable_attached(init_cachify_fixture): From eeb1c5cdbbf307f8e99dc748aa5649c78d852a23 Mon Sep 17 00:00:00 2001 From: Galtozzy Date: Sun, 22 Sep 2024 17:45:45 +0200 Subject: [PATCH 5/5] V/2.0.0 --- py_cachify/__init__.py | 2 +- py_cachify/backend/cached.py | 4 ++-- py_cachify/backend/lock.py | 4 ++-- pyproject.toml | 2 +- sonar-project.properties | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/py_cachify/__init__.py b/py_cachify/__init__.py index a2c776b..5df7efc 100644 --- a/py_cachify/__init__.py +++ b/py_cachify/__init__.py @@ -6,7 +6,7 @@ from .backend.types import Decoder, Encoder -__version__ = '1.2.0' +__version__ = '2.0.0' __all__ = [ 'CachifyInitError', diff --git a/py_cachify/backend/cached.py b/py_cachify/backend/cached.py index e1b3fdc..144c3fb 100644 --- a/py_cachify/backend/cached.py +++ b/py_cachify/backend/cached.py @@ -88,14 +88,14 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return _cached_inner -@deprecated('sync_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') +@deprecated('sync_cached is deprecated, use cached instead. Scheduled for removal in 3.0.0') def sync_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None ) -> SyncOrAsyncReset: return cached(key=key, ttl=ttl, enc_dec=enc_dec) -@deprecated('async_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') +@deprecated('async_cached is deprecated, use cached instead. Scheduled for removal in 3.0.0') def async_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None ) -> SyncOrAsyncReset: diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index 806d4b3..61bb15a 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -274,14 +274,14 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: return _once_inner -@deprecated('sync_once is deprecated, use once instead. Scheduled for removal in 1.3.0') +@deprecated('sync_once is deprecated, use once instead. Scheduled for removal in 3.0.0') def sync_once( key: str, raise_on_locked: bool = False, return_on_locked: Any = None ) -> Callable[[Callable[P, R]], Callable[P, R]]: return once(key=key, raise_on_locked=raise_on_locked, return_on_locked=return_on_locked) -@deprecated('async_once is deprecated, use once instead. Scheduled for removal in 1.3.0') +@deprecated('async_once is deprecated, use once instead. Scheduled for removal in 3.0.0') def async_once( key: str, raise_on_locked: bool = False, return_on_locked: Any = None ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: diff --git a/pyproject.toml b/pyproject.toml index 41586db..d1baced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py-cachify" -version = "1.2.0" +version = "2.0.0" homepage = "https://github.com/EzyGang/py-cachify" repository = "https://github.com/EzyGang/py-cachify" license = "MIT" diff --git a/sonar-project.properties b/sonar-project.properties index d570f19..77aec36 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.organization=ezygang # This is the name and version displayed in the SonarCloud UI. sonar.projectName=py-cachify -sonar.projectVersion=1.2.0 +sonar.projectVersion=2.0.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.