about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/filelock
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/filelock
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/filelock')
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/__init__.py70
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_api.py403
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_error.py30
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_soft.py47
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_unix.py70
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_util.py52
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/_windows.py65
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/asyncio.py342
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/filelock/version.py21
10 files changed, 1100 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/filelock/__init__.py b/.venv/lib/python3.12/site-packages/filelock/__init__.py
new file mode 100644
index 00000000..c9d8c5b8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/__init__.py
@@ -0,0 +1,70 @@
+"""
+A platform independent file lock that supports the with-statement.
+
+.. autodata:: filelock.__version__
+   :no-value:
+
+"""
+
+from __future__ import annotations
+
+import sys
+import warnings
+from typing import TYPE_CHECKING
+
+from ._api import AcquireReturnProxy, BaseFileLock
+from ._error import Timeout
+from ._soft import SoftFileLock
+from ._unix import UnixFileLock, has_fcntl
+from ._windows import WindowsFileLock
+from .asyncio import (
+    AsyncAcquireReturnProxy,
+    AsyncSoftFileLock,
+    AsyncUnixFileLock,
+    AsyncWindowsFileLock,
+    BaseAsyncFileLock,
+)
+from .version import version
+
+#: version of the project as a string
+__version__: str = version
+
+
+if sys.platform == "win32":  # pragma: win32 cover
+    _FileLock: type[BaseFileLock] = WindowsFileLock
+    _AsyncFileLock: type[BaseAsyncFileLock] = AsyncWindowsFileLock
+else:  # pragma: win32 no cover # noqa: PLR5501
+    if has_fcntl:
+        _FileLock: type[BaseFileLock] = UnixFileLock
+        _AsyncFileLock: type[BaseAsyncFileLock] = AsyncUnixFileLock
+    else:
+        _FileLock = SoftFileLock
+        _AsyncFileLock = AsyncSoftFileLock
+        if warnings is not None:
+            warnings.warn("only soft file lock is available", stacklevel=2)
+
+if TYPE_CHECKING:
+    FileLock = SoftFileLock
+    AsyncFileLock = AsyncSoftFileLock
+else:
+    #: Alias for the lock, which should be used for the current platform.
+    FileLock = _FileLock
+    AsyncFileLock = _AsyncFileLock
+
+
+__all__ = [
+    "AcquireReturnProxy",
+    "AsyncAcquireReturnProxy",
+    "AsyncFileLock",
+    "AsyncSoftFileLock",
+    "AsyncUnixFileLock",
+    "AsyncWindowsFileLock",
+    "BaseAsyncFileLock",
+    "BaseFileLock",
+    "FileLock",
+    "SoftFileLock",
+    "Timeout",
+    "UnixFileLock",
+    "WindowsFileLock",
+    "__version__",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_api.py b/.venv/lib/python3.12/site-packages/filelock/_api.py
new file mode 100644
index 00000000..8fde69a0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_api.py
@@ -0,0 +1,403 @@
+from __future__ import annotations
+
+import contextlib
+import inspect
+import logging
+import os
+import time
+import warnings
+from abc import ABCMeta, abstractmethod
+from dataclasses import dataclass
+from threading import local
+from typing import TYPE_CHECKING, Any, cast
+from weakref import WeakValueDictionary
+
+from ._error import Timeout
+
+if TYPE_CHECKING:
+    import sys
+    from types import TracebackType
+
+    if sys.version_info >= (3, 11):  # pragma: no cover (py311+)
+        from typing import Self
+    else:  # pragma: no cover (<py311)
+        from typing_extensions import Self
+
+
+_LOGGER = logging.getLogger("filelock")
+
+
+# This is a helper class which is returned by :meth:`BaseFileLock.acquire` and wraps the lock to make sure __enter__
+# is not called twice when entering the with statement. If we would simply return *self*, the lock would be acquired
+# again in the *__enter__* method of the BaseFileLock, but not released again automatically. issue #37 (memory leak)
+class AcquireReturnProxy:
+    """A context-aware object that will release the lock file when exiting."""
+
+    def __init__(self, lock: BaseFileLock) -> None:
+        self.lock = lock
+
+    def __enter__(self) -> BaseFileLock:
+        return self.lock
+
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
+        self.lock.release()
+
+
+@dataclass
+class FileLockContext:
+    """A dataclass which holds the context for a ``BaseFileLock`` object."""
+
+    # The context is held in a separate class to allow optional use of thread local storage via the
+    # ThreadLocalFileContext class.
+
+    #: The path to the lock file.
+    lock_file: str
+
+    #: The default timeout value.
+    timeout: float
+
+    #: The mode for the lock files
+    mode: int
+
+    #: Whether the lock should be blocking or not
+    blocking: bool
+
+    #: The file descriptor for the *_lock_file* as it is returned by the os.open() function, not None when lock held
+    lock_file_fd: int | None = None
+
+    #: The lock counter is used for implementing the nested locking mechanism.
+    lock_counter: int = 0  # When the lock is acquired is increased and the lock is only released, when this value is 0
+
+
+class ThreadLocalFileContext(FileLockContext, local):
+    """A thread local version of the ``FileLockContext`` class."""
+
+
+class FileLockMeta(ABCMeta):
+    def __call__(  # noqa: PLR0913
+        cls,
+        lock_file: str | os.PathLike[str],
+        timeout: float = -1,
+        mode: int = 0o644,
+        thread_local: bool = True,  # noqa: FBT001, FBT002
+        *,
+        blocking: bool = True,
+        is_singleton: bool = False,
+        **kwargs: Any,  # capture remaining kwargs for subclasses  # noqa: ANN401
+    ) -> BaseFileLock:
+        if is_singleton:
+            instance = cls._instances.get(str(lock_file))  # type: ignore[attr-defined]
+            if instance:
+                params_to_check = {
+                    "thread_local": (thread_local, instance.is_thread_local()),
+                    "timeout": (timeout, instance.timeout),
+                    "mode": (mode, instance.mode),
+                    "blocking": (blocking, instance.blocking),
+                }
+
+                non_matching_params = {
+                    name: (passed_param, set_param)
+                    for name, (passed_param, set_param) in params_to_check.items()
+                    if passed_param != set_param
+                }
+                if not non_matching_params:
+                    return cast("BaseFileLock", instance)
+
+                # parameters do not match; raise error
+                msg = "Singleton lock instances cannot be initialized with differing arguments"
+                msg += "\nNon-matching arguments: "
+                for param_name, (passed_param, set_param) in non_matching_params.items():
+                    msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)"
+                raise ValueError(msg)
+
+        # Workaround to make `__init__`'s params optional in subclasses
+        # E.g. virtualenv changes the signature of the `__init__` method in the `BaseFileLock` class descendant
+        # (https://github.com/tox-dev/filelock/pull/340)
+
+        all_params = {
+            "timeout": timeout,
+            "mode": mode,
+            "thread_local": thread_local,
+            "blocking": blocking,
+            "is_singleton": is_singleton,
+            **kwargs,
+        }
+
+        present_params = inspect.signature(cls.__init__).parameters  # type: ignore[misc]
+        init_params = {key: value for key, value in all_params.items() if key in present_params}
+
+        instance = super().__call__(lock_file, **init_params)
+
+        if is_singleton:
+            cls._instances[str(lock_file)] = instance  # type: ignore[attr-defined]
+
+        return cast("BaseFileLock", instance)
+
+
+class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
+    """Abstract base class for a file lock object."""
+
+    _instances: WeakValueDictionary[str, BaseFileLock]
+
+    def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
+        """Setup unique state for lock subclasses."""
+        super().__init_subclass__(**kwargs)
+        cls._instances = WeakValueDictionary()
+
+    def __init__(  # noqa: PLR0913
+        self,
+        lock_file: str | os.PathLike[str],
+        timeout: float = -1,
+        mode: int = 0o644,
+        thread_local: bool = True,  # noqa: FBT001, FBT002
+        *,
+        blocking: bool = True,
+        is_singleton: bool = False,
+    ) -> None:
+        """
+        Create a new lock object.
+
+        :param lock_file: path to the file
+        :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in \
+            the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it \
+            to a negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock.
+        :param mode: file permissions for the lockfile
+        :param thread_local: Whether this object's internal context should be thread local or not. If this is set to \
+            ``False`` then the lock will be reentrant across threads.
+        :param blocking: whether the lock should be blocking or not
+        :param is_singleton: If this is set to ``True`` then only one instance of this class will be created \
+            per lock file. This is useful if you want to use the lock object for reentrant locking without needing \
+            to pass the same object around.
+
+        """
+        self._is_thread_local = thread_local
+        self._is_singleton = is_singleton
+
+        # Create the context. Note that external code should not work with the context directly and should instead use
+        # properties of this class.
+        kwargs: dict[str, Any] = {
+            "lock_file": os.fspath(lock_file),
+            "timeout": timeout,
+            "mode": mode,
+            "blocking": blocking,
+        }
+        self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(**kwargs)
+
+    def is_thread_local(self) -> bool:
+        """:return: a flag indicating if this lock is thread local or not"""
+        return self._is_thread_local
+
+    @property
+    def is_singleton(self) -> bool:
+        """:return: a flag indicating if this lock is singleton or not"""
+        return self._is_singleton
+
+    @property
+    def lock_file(self) -> str:
+        """:return: path to the lock file"""
+        return self._context.lock_file
+
+    @property
+    def timeout(self) -> float:
+        """
+        :return: the default timeout value, in seconds
+
+        .. versionadded:: 2.0.0
+        """
+        return self._context.timeout
+
+    @timeout.setter
+    def timeout(self, value: float | str) -> None:
+        """
+        Change the default timeout value.
+
+        :param value: the new value, in seconds
+
+        """
+        self._context.timeout = float(value)
+
+    @property
+    def blocking(self) -> bool:
+        """:return: whether the locking is blocking or not"""
+        return self._context.blocking
+
+    @blocking.setter
+    def blocking(self, value: bool) -> None:
+        """
+        Change the default blocking value.
+
+        :param value: the new value as bool
+
+        """
+        self._context.blocking = value
+
+    @property
+    def mode(self) -> int:
+        """:return: the file permissions for the lockfile"""
+        return self._context.mode
+
+    @abstractmethod
+    def _acquire(self) -> None:
+        """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def _release(self) -> None:
+        """Releases the lock and sets self._context.lock_file_fd to None."""
+        raise NotImplementedError
+
+    @property
+    def is_locked(self) -> bool:
+        """
+
+        :return: A boolean indicating if the lock file is holding the lock currently.
+
+        .. versionchanged:: 2.0.0
+
+            This was previously a method and is now a property.
+        """
+        return self._context.lock_file_fd is not None
+
+    @property
+    def lock_counter(self) -> int:
+        """:return: The number of times this lock has been acquired (but not yet released)."""
+        return self._context.lock_counter
+
+    def acquire(
+        self,
+        timeout: float | None = None,
+        poll_interval: float = 0.05,
+        *,
+        poll_intervall: float | None = None,
+        blocking: bool | None = None,
+    ) -> AcquireReturnProxy:
+        """
+        Try to acquire the file lock.
+
+        :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default :attr:`~timeout` is and
+         if ``timeout < 0``, there is no timeout and this method will block until the lock could be acquired
+        :param poll_interval: interval of trying to acquire the lock file
+        :param poll_intervall: deprecated, kept for backwards compatibility, use ``poll_interval`` instead
+        :param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on the
+         first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired.
+        :raises Timeout: if fails to acquire lock within the timeout period
+        :return: a context object that will unlock the file when the context is exited
+
+        .. code-block:: python
+
+            # You can use this method in the context manager (recommended)
+            with lock.acquire():
+                pass
+
+            # Or use an equivalent try-finally construct:
+            lock.acquire()
+            try:
+                pass
+            finally:
+                lock.release()
+
+        .. versionchanged:: 2.0.0
+
+            This method returns now a *proxy* object instead of *self*,
+            so that it can be used in a with statement without side effects.
+
+        """
+        # Use the default timeout, if no timeout is provided.
+        if timeout is None:
+            timeout = self._context.timeout
+
+        if blocking is None:
+            blocking = self._context.blocking
+
+        if poll_intervall is not None:
+            msg = "use poll_interval instead of poll_intervall"
+            warnings.warn(msg, DeprecationWarning, stacklevel=2)
+            poll_interval = poll_intervall
+
+        # Increment the number right at the beginning. We can still undo it, if something fails.
+        self._context.lock_counter += 1
+
+        lock_id = id(self)
+        lock_filename = self.lock_file
+        start_time = time.perf_counter()
+        try:
+            while True:
+                if not self.is_locked:
+                    _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
+                    self._acquire()
+                if self.is_locked:
+                    _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
+                    break
+                if blocking is False:
+                    _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename)
+                    raise Timeout(lock_filename)  # noqa: TRY301
+                if 0 <= timeout < time.perf_counter() - start_time:
+                    _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
+                    raise Timeout(lock_filename)  # noqa: TRY301
+                msg = "Lock %s not acquired on %s, waiting %s seconds ..."
+                _LOGGER.debug(msg, lock_id, lock_filename, poll_interval)
+                time.sleep(poll_interval)
+        except BaseException:  # Something did go wrong, so decrement the counter.
+            self._context.lock_counter = max(0, self._context.lock_counter - 1)
+            raise
+        return AcquireReturnProxy(lock=self)
+
+    def release(self, force: bool = False) -> None:  # noqa: FBT001, FBT002
+        """
+        Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0.
+        Also note, that the lock file itself is not automatically deleted.
+
+        :param force: If true, the lock counter is ignored and the lock is released in every case/
+
+        """
+        if self.is_locked:
+            self._context.lock_counter -= 1
+
+            if self._context.lock_counter == 0 or force:
+                lock_id, lock_filename = id(self), self.lock_file
+
+                _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
+                self._release()
+                self._context.lock_counter = 0
+                _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
+
+    def __enter__(self) -> Self:
+        """
+        Acquire the lock.
+
+        :return: the lock object
+
+        """
+        self.acquire()
+        return self
+
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
+        """
+        Release the lock.
+
+        :param exc_type: the exception type if raised
+        :param exc_value: the exception value if raised
+        :param traceback: the exception traceback if raised
+
+        """
+        self.release()
+
+    def __del__(self) -> None:
+        """Called when the lock object is deleted."""
+        self.release(force=True)
+
+
+__all__ = [
+    "AcquireReturnProxy",
+    "BaseFileLock",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_error.py b/.venv/lib/python3.12/site-packages/filelock/_error.py
new file mode 100644
index 00000000..f7ff08c0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_error.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from typing import Any
+
+
+class Timeout(TimeoutError):  # noqa: N818
+    """Raised when the lock could not be acquired in *timeout* seconds."""
+
+    def __init__(self, lock_file: str) -> None:
+        super().__init__()
+        self._lock_file = lock_file
+
+    def __reduce__(self) -> str | tuple[Any, ...]:
+        return self.__class__, (self._lock_file,)  # Properly pickle the exception
+
+    def __str__(self) -> str:
+        return f"The file lock '{self._lock_file}' could not be acquired."
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.lock_file!r})"
+
+    @property
+    def lock_file(self) -> str:
+        """:return: The path of the file lock."""
+        return self._lock_file
+
+
+__all__ = [
+    "Timeout",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_soft.py b/.venv/lib/python3.12/site-packages/filelock/_soft.py
new file mode 100644
index 00000000..28c67f74
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_soft.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import os
+import sys
+from contextlib import suppress
+from errno import EACCES, EEXIST
+from pathlib import Path
+
+from ._api import BaseFileLock
+from ._util import ensure_directory_exists, raise_on_not_writable_file
+
+
+class SoftFileLock(BaseFileLock):
+    """Simply watches the existence of the lock file."""
+
+    def _acquire(self) -> None:
+        raise_on_not_writable_file(self.lock_file)
+        ensure_directory_exists(self.lock_file)
+        # first check for exists and read-only mode as the open will mask this case as EEXIST
+        flags = (
+            os.O_WRONLY  # open for writing only
+            | os.O_CREAT
+            | os.O_EXCL  # together with above raise EEXIST if the file specified by filename exists
+            | os.O_TRUNC  # truncate the file to zero byte
+        )
+        try:
+            file_handler = os.open(self.lock_file, flags, self._context.mode)
+        except OSError as exception:  # re-raise unless expected exception
+            if not (
+                exception.errno == EEXIST  # lock already exist
+                or (exception.errno == EACCES and sys.platform == "win32")  # has no access to this lock
+            ):  # pragma: win32 no cover
+                raise
+        else:
+            self._context.lock_file_fd = file_handler
+
+    def _release(self) -> None:
+        assert self._context.lock_file_fd is not None  # noqa: S101
+        os.close(self._context.lock_file_fd)  # the lock file is definitely not None
+        self._context.lock_file_fd = None
+        with suppress(OSError):  # the file is already deleted and that's what we want
+            Path(self.lock_file).unlink()
+
+
+__all__ = [
+    "SoftFileLock",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_unix.py b/.venv/lib/python3.12/site-packages/filelock/_unix.py
new file mode 100644
index 00000000..b2fd0f33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_unix.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import os
+import sys
+from contextlib import suppress
+from errno import ENOSYS
+from pathlib import Path
+from typing import cast
+
+from ._api import BaseFileLock
+from ._util import ensure_directory_exists
+
+#: a flag to indicate if the fcntl API is available
+has_fcntl = False
+if sys.platform == "win32":  # pragma: win32 cover
+
+    class UnixFileLock(BaseFileLock):
+        """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
+
+        def _acquire(self) -> None:
+            raise NotImplementedError
+
+        def _release(self) -> None:
+            raise NotImplementedError
+
+else:  # pragma: win32 no cover
+    try:
+        import fcntl
+
+        _ = (fcntl.flock, fcntl.LOCK_EX, fcntl.LOCK_NB, fcntl.LOCK_UN)
+    except (ImportError, AttributeError):
+        pass
+    else:
+        has_fcntl = True
+
+    class UnixFileLock(BaseFileLock):
+        """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
+
+        def _acquire(self) -> None:
+            ensure_directory_exists(self.lock_file)
+            open_flags = os.O_RDWR | os.O_TRUNC
+            if not Path(self.lock_file).exists():
+                open_flags |= os.O_CREAT
+            fd = os.open(self.lock_file, open_flags, self._context.mode)
+            with suppress(PermissionError):  # This locked is not owned by this UID
+                os.fchmod(fd, self._context.mode)
+            try:
+                fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+            except OSError as exception:
+                os.close(fd)
+                if exception.errno == ENOSYS:  # NotImplemented error
+                    msg = "FileSystem does not appear to support flock; use SoftFileLock instead"
+                    raise NotImplementedError(msg) from exception
+            else:
+                self._context.lock_file_fd = fd
+
+        def _release(self) -> None:
+            # Do not remove the lockfile:
+            #   https://github.com/tox-dev/py-filelock/issues/31
+            #   https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
+            fd = cast("int", self._context.lock_file_fd)
+            self._context.lock_file_fd = None
+            fcntl.flock(fd, fcntl.LOCK_UN)
+            os.close(fd)
+
+
+__all__ = [
+    "UnixFileLock",
+    "has_fcntl",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_util.py b/.venv/lib/python3.12/site-packages/filelock/_util.py
new file mode 100644
index 00000000..c671e853
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_util.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import os
+import stat
+import sys
+from errno import EACCES, EISDIR
+from pathlib import Path
+
+
+def raise_on_not_writable_file(filename: str) -> None:
+    """
+    Raise an exception if attempting to open the file for writing would fail.
+
+    This is done so files that will never be writable can be separated from files that are writable but currently
+    locked.
+
+    :param filename: file to check
+    :raises OSError: as if the file was opened for writing.
+
+    """
+    try:  # use stat to do exists + can write to check without race condition
+        file_stat = os.stat(filename)  # noqa: PTH116
+    except OSError:
+        return  # swallow does not exist or other errors
+
+    if file_stat.st_mtime != 0:  # if os.stat returns but modification is zero that's an invalid os.stat - ignore it
+        if not (file_stat.st_mode & stat.S_IWUSR):
+            raise PermissionError(EACCES, "Permission denied", filename)
+
+        if stat.S_ISDIR(file_stat.st_mode):
+            if sys.platform == "win32":  # pragma: win32 cover
+                # On Windows, this is PermissionError
+                raise PermissionError(EACCES, "Permission denied", filename)
+            else:  # pragma: win32 no cover # noqa: RET506
+                # On linux / macOS, this is IsADirectoryError
+                raise IsADirectoryError(EISDIR, "Is a directory", filename)
+
+
+def ensure_directory_exists(filename: Path | str) -> None:
+    """
+    Ensure the directory containing the file exists (create it if necessary).
+
+    :param filename: file.
+
+    """
+    Path(filename).parent.mkdir(parents=True, exist_ok=True)
+
+
+__all__ = [
+    "ensure_directory_exists",
+    "raise_on_not_writable_file",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/_windows.py b/.venv/lib/python3.12/site-packages/filelock/_windows.py
new file mode 100644
index 00000000..348251d1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/_windows.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import os
+import sys
+from contextlib import suppress
+from errno import EACCES
+from pathlib import Path
+from typing import cast
+
+from ._api import BaseFileLock
+from ._util import ensure_directory_exists, raise_on_not_writable_file
+
+if sys.platform == "win32":  # pragma: win32 cover
+    import msvcrt
+
+    class WindowsFileLock(BaseFileLock):
+        """Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""
+
+        def _acquire(self) -> None:
+            raise_on_not_writable_file(self.lock_file)
+            ensure_directory_exists(self.lock_file)
+            flags = (
+                os.O_RDWR  # open for read and write
+                | os.O_CREAT  # create file if not exists
+                | os.O_TRUNC  # truncate file if not empty
+            )
+            try:
+                fd = os.open(self.lock_file, flags, self._context.mode)
+            except OSError as exception:
+                if exception.errno != EACCES:  # has no access to this lock
+                    raise
+            else:
+                try:
+                    msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
+                except OSError as exception:
+                    os.close(fd)  # close file first
+                    if exception.errno != EACCES:  # file is already locked
+                        raise
+                else:
+                    self._context.lock_file_fd = fd
+
+        def _release(self) -> None:
+            fd = cast("int", self._context.lock_file_fd)
+            self._context.lock_file_fd = None
+            msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
+            os.close(fd)
+
+            with suppress(OSError):  # Probably another instance of the application hat acquired the file lock.
+                Path(self.lock_file).unlink()
+
+else:  # pragma: win32 no cover
+
+    class WindowsFileLock(BaseFileLock):
+        """Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""
+
+        def _acquire(self) -> None:
+            raise NotImplementedError
+
+        def _release(self) -> None:
+            raise NotImplementedError
+
+
+__all__ = [
+    "WindowsFileLock",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/asyncio.py b/.venv/lib/python3.12/site-packages/filelock/asyncio.py
new file mode 100644
index 00000000..1c9c9f05
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/asyncio.py
@@ -0,0 +1,342 @@
+"""An asyncio-based implementation of the file lock."""
+
+from __future__ import annotations
+
+import asyncio
+import contextlib
+import logging
+import os
+import time
+from dataclasses import dataclass
+from threading import local
+from typing import TYPE_CHECKING, Any, Callable, NoReturn, cast
+
+from ._api import BaseFileLock, FileLockContext, FileLockMeta
+from ._error import Timeout
+from ._soft import SoftFileLock
+from ._unix import UnixFileLock
+from ._windows import WindowsFileLock
+
+if TYPE_CHECKING:
+    import sys
+    from concurrent import futures
+    from types import TracebackType
+
+    if sys.version_info >= (3, 11):  # pragma: no cover (py311+)
+        from typing import Self
+    else:  # pragma: no cover (<py311)
+        from typing_extensions import Self
+
+
+_LOGGER = logging.getLogger("filelock")
+
+
+@dataclass
+class AsyncFileLockContext(FileLockContext):
+    """A dataclass which holds the context for a ``BaseAsyncFileLock`` object."""
+
+    #: Whether run in executor
+    run_in_executor: bool = True
+
+    #: The executor
+    executor: futures.Executor | None = None
+
+    #: The loop
+    loop: asyncio.AbstractEventLoop | None = None
+
+
+class AsyncThreadLocalFileContext(AsyncFileLockContext, local):
+    """A thread local version of the ``FileLockContext`` class."""
+
+
+class AsyncAcquireReturnProxy:
+    """A context-aware object that will release the lock file when exiting."""
+
+    def __init__(self, lock: BaseAsyncFileLock) -> None:  # noqa: D107
+        self.lock = lock
+
+    async def __aenter__(self) -> BaseAsyncFileLock:  # noqa: D105
+        return self.lock
+
+    async def __aexit__(  # noqa: D105
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
+        await self.lock.release()
+
+
+class AsyncFileLockMeta(FileLockMeta):
+    def __call__(  # type: ignore[override] # noqa: PLR0913
+        cls,  # noqa: N805
+        lock_file: str | os.PathLike[str],
+        timeout: float = -1,
+        mode: int = 0o644,
+        thread_local: bool = False,  # noqa: FBT001, FBT002
+        *,
+        blocking: bool = True,
+        is_singleton: bool = False,
+        loop: asyncio.AbstractEventLoop | None = None,
+        run_in_executor: bool = True,
+        executor: futures.Executor | None = None,
+    ) -> BaseAsyncFileLock:
+        if thread_local and run_in_executor:
+            msg = "run_in_executor is not supported when thread_local is True"
+            raise ValueError(msg)
+        instance = super().__call__(
+            lock_file=lock_file,
+            timeout=timeout,
+            mode=mode,
+            thread_local=thread_local,
+            blocking=blocking,
+            is_singleton=is_singleton,
+            loop=loop,
+            run_in_executor=run_in_executor,
+            executor=executor,
+        )
+        return cast("BaseAsyncFileLock", instance)
+
+
+class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
+    """Base class for asynchronous file locks."""
+
+    def __init__(  # noqa: PLR0913
+        self,
+        lock_file: str | os.PathLike[str],
+        timeout: float = -1,
+        mode: int = 0o644,
+        thread_local: bool = False,  # noqa: FBT001, FBT002
+        *,
+        blocking: bool = True,
+        is_singleton: bool = False,
+        loop: asyncio.AbstractEventLoop | None = None,
+        run_in_executor: bool = True,
+        executor: futures.Executor | None = None,
+    ) -> None:
+        """
+        Create a new lock object.
+
+        :param lock_file: path to the file
+        :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in \
+            the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it \
+            to a negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock.
+        :param mode: file permissions for the lockfile
+        :param thread_local: Whether this object's internal context should be thread local or not. If this is set to \
+            ``False`` then the lock will be reentrant across threads.
+        :param blocking: whether the lock should be blocking or not
+        :param is_singleton: If this is set to ``True`` then only one instance of this class will be created \
+            per lock file. This is useful if you want to use the lock object for reentrant locking without needing \
+            to pass the same object around.
+        :param loop: The event loop to use. If not specified, the running event loop will be used.
+        :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor.
+        :param executor: The executor to use. If not specified, the default executor will be used.
+
+        """
+        self._is_thread_local = thread_local
+        self._is_singleton = is_singleton
+
+        # Create the context. Note that external code should not work with the context directly and should instead use
+        # properties of this class.
+        kwargs: dict[str, Any] = {
+            "lock_file": os.fspath(lock_file),
+            "timeout": timeout,
+            "mode": mode,
+            "blocking": blocking,
+            "loop": loop,
+            "run_in_executor": run_in_executor,
+            "executor": executor,
+        }
+        self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)(
+            **kwargs
+        )
+
+    @property
+    def run_in_executor(self) -> bool:
+        """::return: whether run in executor."""
+        return self._context.run_in_executor
+
+    @property
+    def executor(self) -> futures.Executor | None:
+        """::return: the executor."""
+        return self._context.executor
+
+    @executor.setter
+    def executor(self, value: futures.Executor | None) -> None:  # pragma: no cover
+        """
+        Change the executor.
+
+        :param value: the new executor or ``None``
+        :type value: futures.Executor | None
+
+        """
+        self._context.executor = value
+
+    @property
+    def loop(self) -> asyncio.AbstractEventLoop | None:
+        """::return: the event loop."""
+        return self._context.loop
+
+    async def acquire(  # type: ignore[override]
+        self,
+        timeout: float | None = None,
+        poll_interval: float = 0.05,
+        *,
+        blocking: bool | None = None,
+    ) -> AsyncAcquireReturnProxy:
+        """
+        Try to acquire the file lock.
+
+        :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default
+            :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and
+            this method will block until the lock could be acquired
+        :param poll_interval: interval of trying to acquire the lock file
+        :param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on the
+         first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired.
+        :raises Timeout: if fails to acquire lock within the timeout period
+        :return: a context object that will unlock the file when the context is exited
+
+        .. code-block:: python
+
+            # You can use this method in the context manager (recommended)
+            with lock.acquire():
+                pass
+
+            # Or use an equivalent try-finally construct:
+            lock.acquire()
+            try:
+                pass
+            finally:
+                lock.release()
+
+        """
+        # Use the default timeout, if no timeout is provided.
+        if timeout is None:
+            timeout = self._context.timeout
+
+        if blocking is None:
+            blocking = self._context.blocking
+
+        # Increment the number right at the beginning. We can still undo it, if something fails.
+        self._context.lock_counter += 1
+
+        lock_id = id(self)
+        lock_filename = self.lock_file
+        start_time = time.perf_counter()
+        try:
+            while True:
+                if not self.is_locked:
+                    _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
+                    await self._run_internal_method(self._acquire)
+                if self.is_locked:
+                    _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
+                    break
+                if blocking is False:
+                    _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename)
+                    raise Timeout(lock_filename)  # noqa: TRY301
+                if 0 <= timeout < time.perf_counter() - start_time:
+                    _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
+                    raise Timeout(lock_filename)  # noqa: TRY301
+                msg = "Lock %s not acquired on %s, waiting %s seconds ..."
+                _LOGGER.debug(msg, lock_id, lock_filename, poll_interval)
+                await asyncio.sleep(poll_interval)
+        except BaseException:  # Something did go wrong, so decrement the counter.
+            self._context.lock_counter = max(0, self._context.lock_counter - 1)
+            raise
+        return AsyncAcquireReturnProxy(lock=self)
+
+    async def release(self, force: bool = False) -> None:  # type: ignore[override]  # noqa: FBT001, FBT002
+        """
+        Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0.
+        Also note, that the lock file itself is not automatically deleted.
+
+        :param force: If true, the lock counter is ignored and the lock is released in every case/
+
+        """
+        if self.is_locked:
+            self._context.lock_counter -= 1
+
+            if self._context.lock_counter == 0 or force:
+                lock_id, lock_filename = id(self), self.lock_file
+
+                _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
+                await self._run_internal_method(self._release)
+                self._context.lock_counter = 0
+                _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
+
+    async def _run_internal_method(self, method: Callable[[], Any]) -> None:
+        if asyncio.iscoroutinefunction(method):
+            await method()
+        elif self.run_in_executor:
+            loop = self.loop or asyncio.get_running_loop()
+            await loop.run_in_executor(self.executor, method)
+        else:
+            method()
+
+    def __enter__(self) -> NoReturn:
+        """
+        Replace old __enter__ method to avoid using it.
+
+        NOTE: DO NOT USE `with` FOR ASYNCIO LOCKS, USE `async with` INSTEAD.
+
+        :return: none
+        :rtype: NoReturn
+        """
+        msg = "Do not use `with` for asyncio locks, use `async with` instead."
+        raise NotImplementedError(msg)
+
+    async def __aenter__(self) -> Self:
+        """
+        Acquire the lock.
+
+        :return: the lock object
+
+        """
+        await self.acquire()
+        return self
+
+    async def __aexit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
+        """
+        Release the lock.
+
+        :param exc_type: the exception type if raised
+        :param exc_value: the exception value if raised
+        :param traceback: the exception traceback if raised
+
+        """
+        await self.release()
+
+    def __del__(self) -> None:
+        """Called when the lock object is deleted."""
+        with contextlib.suppress(RuntimeError):
+            loop = self.loop or asyncio.get_running_loop()
+            if not loop.is_running():  # pragma: no cover
+                loop.run_until_complete(self.release(force=True))
+            else:
+                loop.create_task(self.release(force=True))
+
+
+class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock):
+    """Simply watches the existence of the lock file."""
+
+
+class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock):
+    """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
+
+
+class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock):
+    """Uses the :func:`msvcrt.locking` to hard lock the lock file on windows systems."""
+
+
+__all__ = [
+    "AsyncAcquireReturnProxy",
+    "AsyncSoftFileLock",
+    "AsyncUnixFileLock",
+    "AsyncWindowsFileLock",
+    "BaseAsyncFileLock",
+]
diff --git a/.venv/lib/python3.12/site-packages/filelock/py.typed b/.venv/lib/python3.12/site-packages/filelock/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/py.typed
diff --git a/.venv/lib/python3.12/site-packages/filelock/version.py b/.venv/lib/python3.12/site-packages/filelock/version.py
new file mode 100644
index 00000000..68cfbf97
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/filelock/version.py
@@ -0,0 +1,21 @@
+# file generated by setuptools-scm
+# don't change, don't track in version control
+
+__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from typing import Tuple
+    from typing import Union
+
+    VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+    VERSION_TUPLE = object
+
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+
+__version__ = version = '3.18.0'
+__version_tuple__ = version_tuple = (3, 18, 0)