summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/litestar/stores
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.11/site-packages/litestar/stores')
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__init__.py0
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/__init__.cpython-311.pycbin0 -> 199 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/base.cpython-311.pycbin0 -> 7661 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/file.cpython-311.pycbin0 -> 9799 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/memory.cpython-311.pycbin0 -> 7029 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/redis.cpython-311.pycbin0 -> 10088 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/__pycache__/registry.cpython-311.pycbin0 -> 3347 bytes
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/base.py145
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/file.py170
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/memory.py115
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/redis.py204
-rw-r--r--venv/lib/python3.11/site-packages/litestar/stores/registry.py64
12 files changed, 698 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__init__.py b/venv/lib/python3.11/site-packages/litestar/stores/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__init__.py
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..78604d0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/__init__.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/base.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/base.cpython-311.pyc
new file mode 100644
index 0000000..21d2fb4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/base.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/file.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/file.cpython-311.pyc
new file mode 100644
index 0000000..b39333c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/file.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/memory.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/memory.cpython-311.pyc
new file mode 100644
index 0000000..c8e3b05
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/memory.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/redis.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/redis.cpython-311.pyc
new file mode 100644
index 0000000..21e7e2f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/redis.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/registry.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/registry.cpython-311.pyc
new file mode 100644
index 0000000..c87c31a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/__pycache__/registry.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/base.py b/venv/lib/python3.11/site-packages/litestar/stores/base.py
new file mode 100644
index 0000000..34aa514
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/base.py
@@ -0,0 +1,145 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from datetime import datetime, timedelta, timezone
+from typing import TYPE_CHECKING, Optional
+
+from msgspec import Struct
+from msgspec.msgpack import decode as msgpack_decode
+from msgspec.msgpack import encode as msgpack_encode
+
+if TYPE_CHECKING:
+ from types import TracebackType
+
+ from typing_extensions import Self
+
+
+__all__ = ("Store", "NamespacedStore", "StorageObject")
+
+
+class Store(ABC):
+ """Thread and process safe asynchronous key/value store."""
+
+ @abstractmethod
+ async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None:
+ """Set a value.
+
+ Args:
+ key: Key to associate the value with
+ value: Value to store
+ expires_in: Time in seconds before the key is considered expired
+
+ Returns:
+ ``None``
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None:
+ """Get a value.
+
+ Args:
+ key: Key associated with the value
+ renew_for: If given and the value had an initial expiry time set, renew the
+ expiry time for ``renew_for`` seconds. If the value has not been set
+ with an expiry time this is a no-op
+
+ Returns:
+ The value associated with ``key`` if it exists and is not expired, else
+ ``None``
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ async def delete(self, key: str) -> None:
+ """Delete a value.
+
+ If no such key exists, this is a no-op.
+
+ Args:
+ key: Key of the value to delete
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ async def delete_all(self) -> None:
+ """Delete all stored values."""
+ raise NotImplementedError
+
+ @abstractmethod
+ async def exists(self, key: str) -> bool:
+ """Check if a given ``key`` exists."""
+ raise NotImplementedError
+
+ @abstractmethod
+ async def expires_in(self, key: str) -> int | None:
+ """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no
+ expiry time was set, return ``None``.
+ """
+ raise NotImplementedError
+
+ async def __aenter__(self) -> None: # noqa: B027
+ pass
+
+ async def __aexit__( # noqa: B027
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ pass
+
+
+class NamespacedStore(Store):
+ """A subclass of :class:`Store`, offering hierarchical namespacing.
+
+ Bulk actions on a parent namespace should affect all child namespaces, whereas other operations on all namespaces
+ should be isolated.
+ """
+
+ @abstractmethod
+ def with_namespace(self, namespace: str) -> Self:
+ """Return a new instance of :class:`NamespacedStore`, which exists in a child namespace of the current namespace.
+ Bulk actions on the parent namespace should affect all child namespaces, whereas other operations on all
+ namespaces should be isolated.
+ """
+
+
+class StorageObject(Struct):
+ """:class:`msgspec.Struct` to store serialized data alongside with their expiry time."""
+
+ expires_at: Optional[datetime] # noqa: UP007
+ data: bytes
+
+ @classmethod
+ def new(cls, data: bytes, expires_in: int | timedelta | None) -> StorageObject:
+ """Construct a new :class:`StorageObject` instance."""
+ if expires_in is not None and not isinstance(expires_in, timedelta):
+ expires_in = timedelta(seconds=expires_in)
+ return cls(
+ data=data,
+ expires_at=(datetime.now(tz=timezone.utc) + expires_in) if expires_in else None,
+ )
+
+ @property
+ def expired(self) -> bool:
+ """Return if the :class:`StorageObject` is expired"""
+ return self.expires_at is not None and datetime.now(tz=timezone.utc) >= self.expires_at
+
+ @property
+ def expires_in(self) -> int:
+ """Return the expiry time of this ``StorageObject`` in seconds. If no expiry time
+ was set, return ``-1``.
+ """
+ if self.expires_at:
+ return int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
+ return -1
+
+ def to_bytes(self) -> bytes:
+ """Encode the instance to bytes"""
+ return msgpack_encode(self)
+
+ @classmethod
+ def from_bytes(cls, raw: bytes) -> StorageObject:
+ """Load a previously encoded with :meth:`StorageObject.to_bytes`"""
+ return msgpack_decode(raw, type=cls)
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/file.py b/venv/lib/python3.11/site-packages/litestar/stores/file.py
new file mode 100644
index 0000000..25c52eb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/file.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+import os
+import shutil
+import unicodedata
+from tempfile import mkstemp
+from typing import TYPE_CHECKING
+
+from anyio import Path
+
+from litestar.concurrency import sync_to_thread
+
+from .base import NamespacedStore, StorageObject
+
+__all__ = ("FileStore",)
+
+
+if TYPE_CHECKING:
+ from datetime import timedelta
+ from os import PathLike
+
+
+def _safe_file_name(name: str) -> str:
+ name = unicodedata.normalize("NFKD", name)
+ return "".join(c if c.isalnum() else str(ord(c)) for c in name)
+
+
+class FileStore(NamespacedStore):
+ """File based, thread and process safe, asynchronous key/value store."""
+
+ __slots__ = {"path": "file path"}
+
+ def __init__(self, path: PathLike[str]) -> None:
+ """Initialize ``FileStorage``.
+
+ Args:
+ path: Path to store data under
+ """
+ self.path = Path(path)
+
+ def with_namespace(self, namespace: str) -> FileStore:
+ """Return a new instance of :class:`FileStore`, using a sub-path of the current store's path."""
+ if not namespace.isalnum():
+ raise ValueError(f"Invalid namespace: {namespace!r}")
+ return FileStore(self.path / namespace)
+
+ def _path_from_key(self, key: str) -> Path:
+ return self.path / _safe_file_name(key)
+
+ @staticmethod
+ async def _load_from_path(path: Path) -> StorageObject | None:
+ try:
+ data = await path.read_bytes()
+ return StorageObject.from_bytes(data)
+ except FileNotFoundError:
+ return None
+
+ def _write_sync(self, target_file: Path, storage_obj: StorageObject) -> None:
+ try:
+ tmp_file_fd, tmp_file_name = mkstemp(dir=self.path, prefix=f"{target_file.name}.tmp")
+ renamed = False
+ try:
+ try:
+ os.write(tmp_file_fd, storage_obj.to_bytes())
+ finally:
+ os.close(tmp_file_fd)
+
+ os.replace(tmp_file_name, target_file) # noqa: PTH105
+ renamed = True
+ finally:
+ if not renamed:
+ os.unlink(tmp_file_name) # noqa: PTH108
+ except OSError:
+ pass
+
+ async def _write(self, target_file: Path, storage_obj: StorageObject) -> None:
+ await sync_to_thread(self._write_sync, target_file, storage_obj)
+
+ async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None:
+ """Set a value.
+
+ Args:
+ key: Key to associate the value with
+ value: Value to store
+ expires_in: Time in seconds before the key is considered expired
+
+ Returns:
+ ``None``
+ """
+
+ await self.path.mkdir(exist_ok=True)
+ path = self._path_from_key(key)
+ if isinstance(value, str):
+ value = value.encode("utf-8")
+ storage_obj = StorageObject.new(data=value, expires_in=expires_in)
+ await self._write(path, storage_obj)
+
+ async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None:
+ """Get a value.
+
+ Args:
+ key: Key associated with the value
+ renew_for: If given and the value had an initial expiry time set, renew the
+ expiry time for ``renew_for`` seconds. If the value has not been set
+ with an expiry time this is a no-op
+
+ Returns:
+ The value associated with ``key`` if it exists and is not expired, else
+ ``None``
+ """
+ path = self._path_from_key(key)
+ storage_obj = await self._load_from_path(path)
+
+ if not storage_obj:
+ return None
+
+ if storage_obj.expired:
+ await path.unlink(missing_ok=True)
+ return None
+
+ if renew_for and storage_obj.expires_at:
+ await self.set(key, value=storage_obj.data, expires_in=renew_for)
+
+ return storage_obj.data
+
+ async def delete(self, key: str) -> None:
+ """Delete a value.
+
+ If no such key exists, this is a no-op.
+
+ Args:
+ key: Key of the value to delete
+ """
+ path = self._path_from_key(key)
+ await path.unlink(missing_ok=True)
+
+ async def delete_all(self) -> None:
+ """Delete all stored values.
+
+ Note:
+ This deletes and recreates :attr:`FileStore.path`
+ """
+
+ await sync_to_thread(shutil.rmtree, self.path)
+ await self.path.mkdir(exist_ok=True)
+
+ async def delete_expired(self) -> None:
+ """Delete expired items.
+
+ Since expired items are normally only cleared on access (i.e. when calling
+ :meth:`.get`), this method should be called in regular intervals
+ to free disk space.
+ """
+ async for file in self.path.iterdir():
+ wrapper = await self._load_from_path(file)
+ if wrapper and wrapper.expired:
+ await file.unlink(missing_ok=True)
+
+ async def exists(self, key: str) -> bool:
+ """Check if a given ``key`` exists."""
+ path = self._path_from_key(key)
+ return await path.exists()
+
+ async def expires_in(self, key: str) -> int | None:
+ """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no
+ expiry time was set, return ``None``.
+ """
+ if storage_obj := await self._load_from_path(self._path_from_key(key)):
+ return storage_obj.expires_in
+ return None
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/memory.py b/venv/lib/python3.11/site-packages/litestar/stores/memory.py
new file mode 100644
index 0000000..1da8931
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/memory.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import anyio
+from anyio import Lock
+
+from .base import StorageObject, Store
+
+__all__ = ("MemoryStore",)
+
+
+if TYPE_CHECKING:
+ from datetime import timedelta
+
+
+class MemoryStore(Store):
+ """In memory, atomic, asynchronous key/value store."""
+
+ __slots__ = ("_store", "_lock")
+
+ def __init__(self) -> None:
+ """Initialize :class:`MemoryStore`"""
+ self._store: dict[str, StorageObject] = {}
+ self._lock = Lock()
+
+ async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None:
+ """Set a value.
+
+ Args:
+ key: Key to associate the value with
+ value: Value to store
+ expires_in: Time in seconds before the key is considered expired
+
+ Returns:
+ ``None``
+ """
+ if isinstance(value, str):
+ value = value.encode("utf-8")
+ async with self._lock:
+ self._store[key] = StorageObject.new(data=value, expires_in=expires_in)
+
+ async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None:
+ """Get a value.
+
+ Args:
+ key: Key associated with the value
+ renew_for: If given and the value had an initial expiry time set, renew the
+ expiry time for ``renew_for`` seconds. If the value has not been set
+ with an expiry time this is a no-op
+
+ Returns:
+ The value associated with ``key`` if it exists and is not expired, else
+ ``None``
+ """
+ async with self._lock:
+ storage_obj = self._store.get(key)
+
+ if not storage_obj:
+ return None
+
+ if storage_obj.expired:
+ self._store.pop(key)
+ return None
+
+ if renew_for and storage_obj.expires_at:
+ # don't use .set() here, so we can hold onto the lock for the whole operation
+ storage_obj = StorageObject.new(data=storage_obj.data, expires_in=renew_for)
+ self._store[key] = storage_obj
+
+ return storage_obj.data
+
+ async def delete(self, key: str) -> None:
+ """Delete a value.
+
+ If no such key exists, this is a no-op.
+
+ Args:
+ key: Key of the value to delete
+ """
+ async with self._lock:
+ self._store.pop(key, None)
+
+ async def delete_all(self) -> None:
+ """Delete all stored values."""
+ async with self._lock:
+ self._store.clear()
+
+ async def delete_expired(self) -> None:
+ """Delete expired items.
+
+ Since expired items are normally only cleared on access (i.e. when calling
+ :meth:`.get`), this method should be called in regular intervals
+ to free memory.
+ """
+ async with self._lock:
+ new_store = {}
+ for i, (key, storage_obj) in enumerate(self._store.items()):
+ if not storage_obj.expired:
+ new_store[key] = storage_obj
+ if i % 1000 == 0:
+ await anyio.sleep(0)
+ self._store = new_store
+
+ async def exists(self, key: str) -> bool:
+ """Check if a given ``key`` exists."""
+ return key in self._store
+
+ async def expires_in(self, key: str) -> int | None:
+ """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no
+ expiry time was set, return ``None``.
+ """
+ if storage_obj := self._store.get(key):
+ return storage_obj.expires_in
+ return None
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/redis.py b/venv/lib/python3.11/site-packages/litestar/stores/redis.py
new file mode 100644
index 0000000..6697962
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/redis.py
@@ -0,0 +1,204 @@
+from __future__ import annotations
+
+from datetime import timedelta
+from typing import TYPE_CHECKING, cast
+
+from redis.asyncio import Redis
+from redis.asyncio.connection import ConnectionPool
+
+from litestar.exceptions import ImproperlyConfiguredException
+from litestar.types import Empty, EmptyType
+from litestar.utils.empty import value_or_default
+
+from .base import NamespacedStore
+
+__all__ = ("RedisStore",)
+
+if TYPE_CHECKING:
+ from types import TracebackType
+
+
+class RedisStore(NamespacedStore):
+ """Redis based, thread and process safe asynchronous key/value store."""
+
+ __slots__ = ("_redis",)
+
+ def __init__(
+ self, redis: Redis, namespace: str | None | EmptyType = Empty, handle_client_shutdown: bool = False
+ ) -> None:
+ """Initialize :class:`RedisStore`
+
+ Args:
+ redis: An :class:`redis.asyncio.Redis` instance
+ namespace: A key prefix to simulate a namespace in redis. If not given,
+ defaults to ``LITESTAR``. Namespacing can be explicitly disabled by passing
+ ``None``. This will make :meth:`.delete_all` unavailable.
+ handle_client_shutdown: If ``True``, handle the shutdown of the `redis` instance automatically during the store's lifespan. Should be set to `True` unless the shutdown is handled externally
+ """
+ self._redis = redis
+ self.namespace: str | None = value_or_default(namespace, "LITESTAR")
+ self.handle_client_shutdown = handle_client_shutdown
+
+ # script to get and renew a key in one atomic step
+ self._get_and_renew_script = self._redis.register_script(
+ b"""
+ local key = KEYS[1]
+ local renew = tonumber(ARGV[1])
+
+ local data = redis.call('GET', key)
+ local ttl = redis.call('TTL', key)
+
+ if ttl > 0 then
+ redis.call('EXPIRE', key, renew)
+ end
+
+ return data
+ """
+ )
+
+ # script to delete all keys in the namespace
+ self._delete_all_script = self._redis.register_script(
+ b"""
+ local cursor = 0
+
+ repeat
+ local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1])
+ for _,key in ipairs(result[2]) do
+ redis.call('UNLINK', key)
+ end
+ cursor = tonumber(result[1])
+ until cursor == 0
+ """
+ )
+
+ async def _shutdown(self) -> None:
+ if self.handle_client_shutdown:
+ await self._redis.aclose(close_connection_pool=True) # type: ignore[attr-defined]
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ await self._shutdown()
+
+ @classmethod
+ def with_client(
+ cls,
+ url: str = "redis://localhost:6379",
+ *,
+ db: int | None = None,
+ port: int | None = None,
+ username: str | None = None,
+ password: str | None = None,
+ namespace: str | None | EmptyType = Empty,
+ ) -> RedisStore:
+ """Initialize a :class:`RedisStore` instance with a new class:`redis.asyncio.Redis` instance.
+
+ Args:
+ url: Redis URL to connect to
+ db: Redis database to use
+ port: Redis port to use
+ username: Redis username to use
+ password: Redis password to use
+ namespace: Virtual key namespace to use
+ """
+ pool = ConnectionPool.from_url(
+ url=url,
+ db=db,
+ decode_responses=False,
+ port=port,
+ username=username,
+ password=password,
+ )
+ return cls(
+ redis=Redis(connection_pool=pool),
+ namespace=namespace,
+ handle_client_shutdown=True,
+ )
+
+ def with_namespace(self, namespace: str) -> RedisStore:
+ """Return a new :class:`RedisStore` with a nested virtual key namespace.
+ The current instances namespace will serve as a prefix for the namespace, so it
+ can be considered the parent namespace.
+ """
+ return type(self)(
+ redis=self._redis,
+ namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace,
+ handle_client_shutdown=self.handle_client_shutdown,
+ )
+
+ def _make_key(self, key: str) -> str:
+ prefix = f"{self.namespace}:" if self.namespace else ""
+ return prefix + key
+
+ async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None:
+ """Set a value.
+
+ Args:
+ key: Key to associate the value with
+ value: Value to store
+ expires_in: Time in seconds before the key is considered expired
+
+ Returns:
+ ``None``
+ """
+ if isinstance(value, str):
+ value = value.encode("utf-8")
+ await self._redis.set(self._make_key(key), value, ex=expires_in)
+
+ async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None:
+ """Get a value.
+
+ Args:
+ key: Key associated with the value
+ renew_for: If given and the value had an initial expiry time set, renew the
+ expiry time for ``renew_for`` seconds. If the value has not been set
+ with an expiry time this is a no-op. Atomicity of this step is guaranteed
+ by using a lua script to execute fetch and renewal. If ``renew_for`` is
+ not given, the script will be bypassed so no overhead will occur
+
+ Returns:
+ The value associated with ``key`` if it exists and is not expired, else
+ ``None``
+ """
+ key = self._make_key(key)
+ if renew_for:
+ if isinstance(renew_for, timedelta):
+ renew_for = renew_for.seconds
+ data = await self._get_and_renew_script(keys=[key], args=[renew_for])
+ return cast("bytes | None", data)
+ return await self._redis.get(key)
+
+ async def delete(self, key: str) -> None:
+ """Delete a value.
+
+ If no such key exists, this is a no-op.
+
+ Args:
+ key: Key of the value to delete
+ """
+ await self._redis.delete(self._make_key(key))
+
+ async def delete_all(self) -> None:
+ """Delete all stored values in the virtual key namespace.
+
+ Raises:
+ ImproperlyConfiguredException: If no namespace was configured
+ """
+ if not self.namespace:
+ raise ImproperlyConfiguredException("Cannot perform delete operation: No namespace configured")
+
+ await self._delete_all_script(keys=[], args=[f"{self.namespace}*:*"])
+
+ async def exists(self, key: str) -> bool:
+ """Check if a given ``key`` exists."""
+ return await self._redis.exists(self._make_key(key)) == 1
+
+ async def expires_in(self, key: str) -> int | None:
+ """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no
+ expiry time was set, return ``None``.
+ """
+ ttl = await self._redis.ttl(self._make_key(key))
+ return None if ttl == -2 else ttl
diff --git a/venv/lib/python3.11/site-packages/litestar/stores/registry.py b/venv/lib/python3.11/site-packages/litestar/stores/registry.py
new file mode 100644
index 0000000..11a08c2
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/stores/registry.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+if TYPE_CHECKING:
+ from .base import Store
+
+
+from .memory import MemoryStore
+
+__all__ = ("StoreRegistry",)
+
+
+def default_default_factory(name: str) -> Store:
+ return MemoryStore()
+
+
+class StoreRegistry:
+ """Registry for :class:`Store <.base.Store>` instances."""
+
+ __slots__ = ("_stores", "_default_factory")
+
+ def __init__(
+ self, stores: dict[str, Store] | None = None, default_factory: Callable[[str], Store] = default_default_factory
+ ) -> None:
+ """Initialize ``StoreRegistry``.
+
+ Args:
+ stores: A dictionary mapping store names to stores, used to initialize the registry
+ default_factory: A callable used by :meth:`StoreRegistry.get` to provide a store, if the requested name hasn't
+ been registered yet. This callable receives the requested name and should return a
+ :class:`Store <.base.Store>` instance.
+ """
+ self._stores = stores or {}
+ self._default_factory = default_factory
+
+ def register(self, name: str, store: Store, allow_override: bool = False) -> None:
+ """Register a new :class:`Store <.base.Store>`.
+
+ Args:
+ name: Name to register the store under
+ store: The store to register
+ allow_override: Whether to allow overriding an existing store of the same name
+
+ Raises:
+ ValueError: If a store is already registered under this name and ``override`` is not ``True``
+ """
+ if not allow_override and name in self._stores:
+ raise ValueError(f"Store with the name {name!r} already exists")
+ self._stores[name] = store
+
+ def get(self, name: str) -> Store:
+ """Get a store registered under ``name``. If no such store is registered, create a store using the default
+ factory with ``name`` and register the returned store under ``name``.
+
+ Args:
+ name: Name of the store
+
+ Returns:
+ A :class:`Store <.base.Store>`
+ """
+ if not self._stores.get(name):
+ self._stores[name] = self._default_factory(name)
+ return self._stores[name]