about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/cachetools
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/cachetools')
-rw-r--r--.venv/lib/python3.12/site-packages/cachetools/__init__.py738
-rw-r--r--.venv/lib/python3.12/site-packages/cachetools/_decorators.py152
-rw-r--r--.venv/lib/python3.12/site-packages/cachetools/func.py121
-rw-r--r--.venv/lib/python3.12/site-packages/cachetools/keys.py62
4 files changed, 1073 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/cachetools/__init__.py b/.venv/lib/python3.12/site-packages/cachetools/__init__.py
new file mode 100644
index 00000000..d3051c10
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/cachetools/__init__.py
@@ -0,0 +1,738 @@
+"""Extensible memoizing collections and decorators."""
+
+__all__ = (
+    "Cache",
+    "FIFOCache",
+    "LFUCache",
+    "LRUCache",
+    "MRUCache",
+    "RRCache",
+    "TLRUCache",
+    "TTLCache",
+    "cached",
+    "cachedmethod",
+)
+
+__version__ = "5.5.2"
+
+import collections
+import collections.abc
+import functools
+import heapq
+import random
+import time
+
+from . import keys
+from ._decorators import _cached_wrapper
+
+
+class _DefaultSize:
+    __slots__ = ()
+
+    def __getitem__(self, _):
+        return 1
+
+    def __setitem__(self, _, value):
+        assert value == 1
+
+    def pop(self, _):
+        return 1
+
+
+class Cache(collections.abc.MutableMapping):
+    """Mutable mapping to serve as a simple cache or cache base class."""
+
+    __marker = object()
+
+    __size = _DefaultSize()
+
+    def __init__(self, maxsize, getsizeof=None):
+        if getsizeof:
+            self.getsizeof = getsizeof
+        if self.getsizeof is not Cache.getsizeof:
+            self.__size = dict()
+        self.__data = dict()
+        self.__currsize = 0
+        self.__maxsize = maxsize
+
+    def __repr__(self):
+        return "%s(%s, maxsize=%r, currsize=%r)" % (
+            self.__class__.__name__,
+            repr(self.__data),
+            self.__maxsize,
+            self.__currsize,
+        )
+
+    def __getitem__(self, key):
+        try:
+            return self.__data[key]
+        except KeyError:
+            return self.__missing__(key)
+
+    def __setitem__(self, key, value):
+        maxsize = self.__maxsize
+        size = self.getsizeof(value)
+        if size > maxsize:
+            raise ValueError("value too large")
+        if key not in self.__data or self.__size[key] < size:
+            while self.__currsize + size > maxsize:
+                self.popitem()
+        if key in self.__data:
+            diffsize = size - self.__size[key]
+        else:
+            diffsize = size
+        self.__data[key] = value
+        self.__size[key] = size
+        self.__currsize += diffsize
+
+    def __delitem__(self, key):
+        size = self.__size.pop(key)
+        del self.__data[key]
+        self.__currsize -= size
+
+    def __contains__(self, key):
+        return key in self.__data
+
+    def __missing__(self, key):
+        raise KeyError(key)
+
+    def __iter__(self):
+        return iter(self.__data)
+
+    def __len__(self):
+        return len(self.__data)
+
+    def get(self, key, default=None):
+        if key in self:
+            return self[key]
+        else:
+            return default
+
+    def pop(self, key, default=__marker):
+        if key in self:
+            value = self[key]
+            del self[key]
+        elif default is self.__marker:
+            raise KeyError(key)
+        else:
+            value = default
+        return value
+
+    def setdefault(self, key, default=None):
+        if key in self:
+            value = self[key]
+        else:
+            self[key] = value = default
+        return value
+
+    @property
+    def maxsize(self):
+        """The maximum size of the cache."""
+        return self.__maxsize
+
+    @property
+    def currsize(self):
+        """The current size of the cache."""
+        return self.__currsize
+
+    @staticmethod
+    def getsizeof(value):
+        """Return the size of a cache element's value."""
+        return 1
+
+
+class FIFOCache(Cache):
+    """First In First Out (FIFO) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        try:
+            self.__order.move_to_end(key)
+        except KeyError:
+            self.__order[key] = None
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair first inserted."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class LFUCache(Cache):
+    """Least Frequently Used (LFU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__counter = collections.Counter()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__counter[key] -= 1
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__counter[key] -= 1
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__counter[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least frequently used."""
+        try:
+            ((key, _),) = self.__counter.most_common(1)
+        except ValueError:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class LRUCache(Cache):
+    """Least Recently Used (LRU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__update(key)
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__update(key)
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least recently used."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+    def __update(self, key):
+        try:
+            self.__order.move_to_end(key)
+        except KeyError:
+            self.__order[key] = None
+
+
+class MRUCache(Cache):
+    """Most Recently Used (MRU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        from warnings import warn
+
+        warn("MRUCache is deprecated", DeprecationWarning, stacklevel=2)
+
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__update(key)
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__update(key)
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair most recently used."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+    def __update(self, key):
+        try:
+            self.__order.move_to_end(key, last=False)
+        except KeyError:
+            self.__order[key] = None
+
+
+class RRCache(Cache):
+    """Random Replacement (RR) cache implementation."""
+
+    def __init__(self, maxsize, choice=random.choice, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__choice = choice
+
+    @property
+    def choice(self):
+        """The `choice` function used by the cache."""
+        return self.__choice
+
+    def popitem(self):
+        """Remove and return a random `(key, value)` pair."""
+        try:
+            key = self.__choice(list(self))
+        except IndexError:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class _TimedCache(Cache):
+    """Base class for time aware cache implementations."""
+
+    class _Timer:
+        def __init__(self, timer):
+            self.__timer = timer
+            self.__nesting = 0
+
+        def __call__(self):
+            if self.__nesting == 0:
+                return self.__timer()
+            else:
+                return self.__time
+
+        def __enter__(self):
+            if self.__nesting == 0:
+                self.__time = time = self.__timer()
+            else:
+                time = self.__time
+            self.__nesting += 1
+            return time
+
+        def __exit__(self, *exc):
+            self.__nesting -= 1
+
+        def __reduce__(self):
+            return _TimedCache._Timer, (self.__timer,)
+
+        def __getattr__(self, name):
+            return getattr(self.__timer, name)
+
+    def __init__(self, maxsize, timer=time.monotonic, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__timer = _TimedCache._Timer(timer)
+
+    def __repr__(self, cache_repr=Cache.__repr__):
+        with self.__timer as time:
+            self.expire(time)
+            return cache_repr(self)
+
+    def __len__(self, cache_len=Cache.__len__):
+        with self.__timer as time:
+            self.expire(time)
+            return cache_len(self)
+
+    @property
+    def currsize(self):
+        with self.__timer as time:
+            self.expire(time)
+            return super().currsize
+
+    @property
+    def timer(self):
+        """The timer function used by the cache."""
+        return self.__timer
+
+    def clear(self):
+        with self.__timer as time:
+            self.expire(time)
+            Cache.clear(self)
+
+    def get(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.get(self, *args, **kwargs)
+
+    def pop(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.pop(self, *args, **kwargs)
+
+    def setdefault(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.setdefault(self, *args, **kwargs)
+
+
+class TTLCache(_TimedCache):
+    """LRU Cache implementation with per-item time-to-live (TTL) value."""
+
+    class _Link:
+        __slots__ = ("key", "expires", "next", "prev")
+
+        def __init__(self, key=None, expires=None):
+            self.key = key
+            self.expires = expires
+
+        def __reduce__(self):
+            return TTLCache._Link, (self.key, self.expires)
+
+        def unlink(self):
+            next = self.next
+            prev = self.prev
+            prev.next = next
+            next.prev = prev
+
+    def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
+        _TimedCache.__init__(self, maxsize, timer, getsizeof)
+        self.__root = root = TTLCache._Link()
+        root.prev = root.next = root
+        self.__links = collections.OrderedDict()
+        self.__ttl = ttl
+
+    def __contains__(self, key):
+        try:
+            link = self.__links[key]  # no reordering
+        except KeyError:
+            return False
+        else:
+            return self.timer() < link.expires
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        try:
+            link = self.__getlink(key)
+        except KeyError:
+            expired = False
+        else:
+            expired = not (self.timer() < link.expires)
+        if expired:
+            return self.__missing__(key)
+        else:
+            return cache_getitem(self, key)
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        with self.timer as time:
+            self.expire(time)
+            cache_setitem(self, key, value)
+        try:
+            link = self.__getlink(key)
+        except KeyError:
+            self.__links[key] = link = TTLCache._Link(key)
+        else:
+            link.unlink()
+        link.expires = time + self.__ttl
+        link.next = root = self.__root
+        link.prev = prev = root.prev
+        prev.next = root.prev = link
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        link = self.__links.pop(key)
+        link.unlink()
+        if not (self.timer() < link.expires):
+            raise KeyError(key)
+
+    def __iter__(self):
+        root = self.__root
+        curr = root.next
+        while curr is not root:
+            # "freeze" time for iterator access
+            with self.timer as time:
+                if time < curr.expires:
+                    yield curr.key
+            curr = curr.next
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        root = self.__root
+        root.prev = root.next = root
+        for link in sorted(self.__links.values(), key=lambda obj: obj.expires):
+            link.next = root
+            link.prev = prev = root.prev
+            prev.next = root.prev = link
+        self.expire(self.timer())
+
+    @property
+    def ttl(self):
+        """The time-to-live value of the cache's items."""
+        return self.__ttl
+
+    def expire(self, time=None):
+        """Remove expired items from the cache and return an iterable of the
+        expired `(key, value)` pairs.
+
+        """
+        if time is None:
+            time = self.timer()
+        root = self.__root
+        curr = root.next
+        links = self.__links
+        expired = []
+        cache_delitem = Cache.__delitem__
+        cache_getitem = Cache.__getitem__
+        while curr is not root and not (time < curr.expires):
+            expired.append((curr.key, cache_getitem(self, curr.key)))
+            cache_delitem(self, curr.key)
+            del links[curr.key]
+            next = curr.next
+            curr.unlink()
+            curr = next
+        return expired
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least recently used that
+        has not already expired.
+
+        """
+        with self.timer as time:
+            self.expire(time)
+            try:
+                key = next(iter(self.__links))
+            except StopIteration:
+                raise KeyError("%s is empty" % type(self).__name__) from None
+            else:
+                return (key, self.pop(key))
+
+    def __getlink(self, key):
+        value = self.__links[key]
+        self.__links.move_to_end(key)
+        return value
+
+
+class TLRUCache(_TimedCache):
+    """Time aware Least Recently Used (TLRU) cache implementation."""
+
+    @functools.total_ordering
+    class _Item:
+        __slots__ = ("key", "expires", "removed")
+
+        def __init__(self, key=None, expires=None):
+            self.key = key
+            self.expires = expires
+            self.removed = False
+
+        def __lt__(self, other):
+            return self.expires < other.expires
+
+    def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
+        _TimedCache.__init__(self, maxsize, timer, getsizeof)
+        self.__items = collections.OrderedDict()
+        self.__order = []
+        self.__ttu = ttu
+
+    def __contains__(self, key):
+        try:
+            item = self.__items[key]  # no reordering
+        except KeyError:
+            return False
+        else:
+            return self.timer() < item.expires
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        try:
+            item = self.__getitem(key)
+        except KeyError:
+            expired = False
+        else:
+            expired = not (self.timer() < item.expires)
+        if expired:
+            return self.__missing__(key)
+        else:
+            return cache_getitem(self, key)
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        with self.timer as time:
+            expires = self.__ttu(key, value, time)
+            if not (time < expires):
+                return  # skip expired items
+            self.expire(time)
+            cache_setitem(self, key, value)
+        # removing an existing item would break the heap structure, so
+        # only mark it as removed for now
+        try:
+            self.__getitem(key).removed = True
+        except KeyError:
+            pass
+        self.__items[key] = item = TLRUCache._Item(key, expires)
+        heapq.heappush(self.__order, item)
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        with self.timer as time:
+            # no self.expire() for performance reasons, e.g. self.clear() [#67]
+            cache_delitem(self, key)
+        item = self.__items.pop(key)
+        item.removed = True
+        if not (time < item.expires):
+            raise KeyError(key)
+
+    def __iter__(self):
+        for curr in self.__order:
+            # "freeze" time for iterator access
+            with self.timer as time:
+                if time < curr.expires and not curr.removed:
+                    yield curr.key
+
+    @property
+    def ttu(self):
+        """The local time-to-use function used by the cache."""
+        return self.__ttu
+
+    def expire(self, time=None):
+        """Remove expired items from the cache and return an iterable of the
+        expired `(key, value)` pairs.
+
+        """
+        if time is None:
+            time = self.timer()
+        items = self.__items
+        order = self.__order
+        # clean up the heap if too many items are marked as removed
+        if len(order) > len(items) * 2:
+            self.__order = order = [item for item in order if not item.removed]
+            heapq.heapify(order)
+        expired = []
+        cache_delitem = Cache.__delitem__
+        cache_getitem = Cache.__getitem__
+        while order and (order[0].removed or not (time < order[0].expires)):
+            item = heapq.heappop(order)
+            if not item.removed:
+                expired.append((item.key, cache_getitem(self, item.key)))
+                cache_delitem(self, item.key)
+                del items[item.key]
+        return expired
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least recently used that
+        has not already expired.
+
+        """
+        with self.timer as time:
+            self.expire(time)
+            try:
+                key = next(iter(self.__items))
+            except StopIteration:
+                raise KeyError("%s is empty" % self.__class__.__name__) from None
+            else:
+                return (key, self.pop(key))
+
+    def __getitem(self, key):
+        value = self.__items[key]
+        self.__items.move_to_end(key)
+        return value
+
+
+_CacheInfo = collections.namedtuple(
+    "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
+)
+
+
+def cached(cache, key=keys.hashkey, lock=None, info=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    results in a cache.
+
+    """
+
+    def decorator(func):
+        if info:
+            if isinstance(cache, Cache):
+
+                def make_info(hits, misses):
+                    return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
+
+            elif isinstance(cache, collections.abc.Mapping):
+
+                def make_info(hits, misses):
+                    return _CacheInfo(hits, misses, None, len(cache))
+
+            else:
+
+                def make_info(hits, misses):
+                    return _CacheInfo(hits, misses, 0, 0)
+
+            wrapper = _cached_wrapper(func, cache, key, lock, make_info)
+        else:
+            wrapper = _cached_wrapper(func, cache, key, lock, None)
+
+        wrapper.cache = cache
+        wrapper.cache_key = key
+        wrapper.cache_lock = lock
+
+        return functools.update_wrapper(wrapper, func)
+
+    return decorator
+
+
+def cachedmethod(cache, key=keys.methodkey, lock=None):
+    """Decorator to wrap a class or instance method with a memoizing
+    callable that saves results in a cache.
+
+    """
+
+    def decorator(method):
+        if lock is None:
+
+            def wrapper(self, *args, **kwargs):
+                c = cache(self)
+                if c is None:
+                    return method(self, *args, **kwargs)
+                k = key(self, *args, **kwargs)
+                try:
+                    return c[k]
+                except KeyError:
+                    pass  # key not found
+                v = method(self, *args, **kwargs)
+                try:
+                    c[k] = v
+                except ValueError:
+                    pass  # value too large
+                return v
+
+            def clear(self):
+                c = cache(self)
+                if c is not None:
+                    c.clear()
+
+        else:
+
+            def wrapper(self, *args, **kwargs):
+                c = cache(self)
+                if c is None:
+                    return method(self, *args, **kwargs)
+                k = key(self, *args, **kwargs)
+                try:
+                    with lock(self):
+                        return c[k]
+                except KeyError:
+                    pass  # key not found
+                v = method(self, *args, **kwargs)
+                # in case of a race, prefer the item already in the cache
+                try:
+                    with lock(self):
+                        return c.setdefault(k, v)
+                except ValueError:
+                    return v  # value too large
+
+            def clear(self):
+                c = cache(self)
+                if c is not None:
+                    with lock(self):
+                        c.clear()
+
+        wrapper.cache = cache
+        wrapper.cache_key = key
+        wrapper.cache_lock = lock
+        wrapper.cache_clear = clear
+
+        return functools.update_wrapper(wrapper, method)
+
+    return decorator
diff --git a/.venv/lib/python3.12/site-packages/cachetools/_decorators.py b/.venv/lib/python3.12/site-packages/cachetools/_decorators.py
new file mode 100644
index 00000000..fcac1e01
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/cachetools/_decorators.py
@@ -0,0 +1,152 @@
+"""Extensible memoizing decorator helpers."""
+
+
+def _cached_locked_info(func, cache, key, lock, info):
+    hits = misses = 0
+
+    def wrapper(*args, **kwargs):
+        nonlocal hits, misses
+        k = key(*args, **kwargs)
+        with lock:
+            try:
+                result = cache[k]
+                hits += 1
+                return result
+            except KeyError:
+                misses += 1
+        v = func(*args, **kwargs)
+        with lock:
+            try:
+                # in case of a race, prefer the item already in the cache
+                return cache.setdefault(k, v)
+            except ValueError:
+                return v  # value too large
+
+    def cache_clear():
+        nonlocal hits, misses
+        with lock:
+            cache.clear()
+            hits = misses = 0
+
+    def cache_info():
+        with lock:
+            return info(hits, misses)
+
+    wrapper.cache_clear = cache_clear
+    wrapper.cache_info = cache_info
+    return wrapper
+
+
+def _cached_unlocked_info(func, cache, key, info):
+    hits = misses = 0
+
+    def wrapper(*args, **kwargs):
+        nonlocal hits, misses
+        k = key(*args, **kwargs)
+        try:
+            result = cache[k]
+            hits += 1
+            return result
+        except KeyError:
+            misses += 1
+        v = func(*args, **kwargs)
+        try:
+            cache[k] = v
+        except ValueError:
+            pass  # value too large
+        return v
+
+    def cache_clear():
+        nonlocal hits, misses
+        cache.clear()
+        hits = misses = 0
+
+    wrapper.cache_clear = cache_clear
+    wrapper.cache_info = lambda: info(hits, misses)
+    return wrapper
+
+
+def _uncached_info(func, info):
+    misses = 0
+
+    def wrapper(*args, **kwargs):
+        nonlocal misses
+        misses += 1
+        return func(*args, **kwargs)
+
+    def cache_clear():
+        nonlocal misses
+        misses = 0
+
+    wrapper.cache_clear = cache_clear
+    wrapper.cache_info = lambda: info(0, misses)
+    return wrapper
+
+
+def _cached_locked(func, cache, key, lock):
+    def wrapper(*args, **kwargs):
+        k = key(*args, **kwargs)
+        with lock:
+            try:
+                return cache[k]
+            except KeyError:
+                pass  # key not found
+        v = func(*args, **kwargs)
+        with lock:
+            try:
+                # in case of a race, prefer the item already in the cache
+                return cache.setdefault(k, v)
+            except ValueError:
+                return v  # value too large
+
+    def cache_clear():
+        with lock:
+            cache.clear()
+
+    wrapper.cache_clear = cache_clear
+    return wrapper
+
+
+def _cached_unlocked(func, cache, key):
+    def wrapper(*args, **kwargs):
+        k = key(*args, **kwargs)
+        try:
+            return cache[k]
+        except KeyError:
+            pass  # key not found
+        v = func(*args, **kwargs)
+        try:
+            cache[k] = v
+        except ValueError:
+            pass  # value too large
+        return v
+
+    wrapper.cache_clear = lambda: cache.clear()
+    return wrapper
+
+
+def _uncached(func):
+    def wrapper(*args, **kwargs):
+        return func(*args, **kwargs)
+
+    wrapper.cache_clear = lambda: None
+    return wrapper
+
+
+def _cached_wrapper(func, cache, key, lock, info):
+    if info is not None:
+        if cache is None:
+            wrapper = _uncached_info(func, info)
+        elif lock is None:
+            wrapper = _cached_unlocked_info(func, cache, key, info)
+        else:
+            wrapper = _cached_locked_info(func, cache, key, lock, info)
+    else:
+        if cache is None:
+            wrapper = _uncached(func)
+        elif lock is None:
+            wrapper = _cached_unlocked(func, cache, key)
+        else:
+            wrapper = _cached_locked(func, cache, key, lock)
+        wrapper.cache_info = None
+    return wrapper
diff --git a/.venv/lib/python3.12/site-packages/cachetools/func.py b/.venv/lib/python3.12/site-packages/cachetools/func.py
new file mode 100644
index 00000000..3eafddf1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/cachetools/func.py
@@ -0,0 +1,121 @@
+"""`functools.lru_cache` compatible memoizing function decorators."""
+
+__all__ = ("fifo_cache", "lfu_cache", "lru_cache", "mru_cache", "rr_cache", "ttl_cache")
+
+import math
+import random
+import time
+
+try:
+    from threading import RLock
+except ImportError:  # pragma: no cover
+    from dummy_threading import RLock
+
+from . import FIFOCache, LFUCache, LRUCache, MRUCache, RRCache, TTLCache
+from . import cached
+from . import keys
+
+
+class _UnboundTTLCache(TTLCache):
+    def __init__(self, ttl, timer):
+        TTLCache.__init__(self, math.inf, ttl, timer)
+
+    @property
+    def maxsize(self):
+        return None
+
+
+def _cache(cache, maxsize, typed):
+    def decorator(func):
+        key = keys.typedkey if typed else keys.hashkey
+        wrapper = cached(cache=cache, key=key, lock=RLock(), info=True)(func)
+        wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed}
+        return wrapper
+
+    return decorator
+
+
+def fifo_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a First In First Out (FIFO)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache({}, None, typed)
+    elif callable(maxsize):
+        return _cache(FIFOCache(128), 128, typed)(maxsize)
+    else:
+        return _cache(FIFOCache(maxsize), maxsize, typed)
+
+
+def lfu_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Frequently Used (LFU)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache({}, None, typed)
+    elif callable(maxsize):
+        return _cache(LFUCache(128), 128, typed)(maxsize)
+    else:
+        return _cache(LFUCache(maxsize), maxsize, typed)
+
+
+def lru_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Recently Used (LRU)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache({}, None, typed)
+    elif callable(maxsize):
+        return _cache(LRUCache(128), 128, typed)(maxsize)
+    else:
+        return _cache(LRUCache(maxsize), maxsize, typed)
+
+
+def mru_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Most Recently Used (MRU)
+    algorithm.
+    """
+    from warnings import warn
+
+    warn("@mru_cache is deprecated", DeprecationWarning, stacklevel=2)
+
+    if maxsize is None:
+        return _cache({}, None, typed)
+    elif callable(maxsize):
+        return _cache(MRUCache(128), 128, typed)(maxsize)
+    else:
+        return _cache(MRUCache(maxsize), maxsize, typed)
+
+
+def rr_cache(maxsize=128, choice=random.choice, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Random Replacement (RR)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache({}, None, typed)
+    elif callable(maxsize):
+        return _cache(RRCache(128, choice), 128, typed)(maxsize)
+    else:
+        return _cache(RRCache(maxsize, choice), maxsize, typed)
+
+
+def ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Recently Used (LRU)
+    algorithm with a per-item time-to-live (TTL) value.
+    """
+    if maxsize is None:
+        return _cache(_UnboundTTLCache(ttl, timer), None, typed)
+    elif callable(maxsize):
+        return _cache(TTLCache(128, ttl, timer), 128, typed)(maxsize)
+    else:
+        return _cache(TTLCache(maxsize, ttl, timer), maxsize, typed)
diff --git a/.venv/lib/python3.12/site-packages/cachetools/keys.py b/.venv/lib/python3.12/site-packages/cachetools/keys.py
new file mode 100644
index 00000000..8689b17b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/cachetools/keys.py
@@ -0,0 +1,62 @@
+"""Key functions for memoizing decorators."""
+
+__all__ = ("hashkey", "methodkey", "typedkey", "typedmethodkey")
+
+
+class _HashedTuple(tuple):
+    """A tuple that ensures that hash() will be called no more than once
+    per element, since cache decorators will hash the key multiple
+    times on a cache miss.  See also _HashedSeq in the standard
+    library functools implementation.
+
+    """
+
+    __hashvalue = None
+
+    def __hash__(self, hash=tuple.__hash__):
+        hashvalue = self.__hashvalue
+        if hashvalue is None:
+            self.__hashvalue = hashvalue = hash(self)
+        return hashvalue
+
+    def __add__(self, other, add=tuple.__add__):
+        return _HashedTuple(add(self, other))
+
+    def __radd__(self, other, add=tuple.__add__):
+        return _HashedTuple(add(other, self))
+
+    def __getstate__(self):
+        return {}
+
+
+# used for separating keyword arguments; we do not use an object
+# instance here so identity is preserved when pickling/unpickling
+_kwmark = (_HashedTuple,)
+
+
+def hashkey(*args, **kwargs):
+    """Return a cache key for the specified hashable arguments."""
+
+    if kwargs:
+        return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark))
+    else:
+        return _HashedTuple(args)
+
+
+def methodkey(self, *args, **kwargs):
+    """Return a cache key for use with cached methods."""
+    return hashkey(*args, **kwargs)
+
+
+def typedkey(*args, **kwargs):
+    """Return a typed cache key for the specified hashable arguments."""
+
+    key = hashkey(*args, **kwargs)
+    key += tuple(type(v) for v in args)
+    key += tuple(type(v) for _, v in sorted(kwargs.items()))
+    return key
+
+
+def typedmethodkey(self, *args, **kwargs):
+    """Return a typed cache key for use with cached methods."""
+    return typedkey(*args, **kwargs)