Mise à jour de Monitor.py et autres scripts
This commit is contained in:
122
myenv/lib/python3.11/site-packages/watchdog/utils/__init__.py
Normal file
122
myenv/lib/python3.11/site-packages/watchdog/utils/__init__.py
Normal file
@@ -0,0 +1,122 @@
|
||||
""":module: watchdog.utils
|
||||
:synopsis: Utility classes and functions.
|
||||
:author: yesudeep@google.com (Yesudeep Mangalapilly)
|
||||
:author: contact@tiger-222.fr (Mickaël Schoentgen)
|
||||
|
||||
Classes
|
||||
-------
|
||||
.. autoclass:: BaseThread
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
from watchdog.tricks import Trick
|
||||
|
||||
|
||||
class UnsupportedLibcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WatchdogShutdownError(Exception):
|
||||
"""Semantic exception used to signal an external shutdown event."""
|
||||
|
||||
|
||||
class BaseThread(threading.Thread):
|
||||
"""Convenience class for creating stoppable threads."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
threading.Thread.__init__(self)
|
||||
if hasattr(self, "daemon"):
|
||||
self.daemon = True
|
||||
else:
|
||||
self.setDaemon(True)
|
||||
self._stopped_event = threading.Event()
|
||||
|
||||
@property
|
||||
def stopped_event(self) -> threading.Event:
|
||||
return self._stopped_event
|
||||
|
||||
def should_keep_running(self) -> bool:
|
||||
"""Determines whether the thread should continue running."""
|
||||
return not self._stopped_event.is_set()
|
||||
|
||||
def on_thread_stop(self) -> None:
|
||||
"""Override this method instead of :meth:`stop()`.
|
||||
:meth:`stop()` calls this method.
|
||||
|
||||
This method is called immediately after the thread is signaled to stop.
|
||||
"""
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Signals the thread to stop."""
|
||||
self._stopped_event.set()
|
||||
self.on_thread_stop()
|
||||
|
||||
def on_thread_start(self) -> None:
|
||||
"""Override this method instead of :meth:`start()`. :meth:`start()`
|
||||
calls this method.
|
||||
|
||||
This method is called right before this thread is started and this
|
||||
object's run() method is invoked.
|
||||
"""
|
||||
|
||||
def start(self) -> None:
|
||||
self.on_thread_start()
|
||||
threading.Thread.start(self)
|
||||
|
||||
|
||||
def load_module(module_name: str) -> ModuleType:
|
||||
"""Imports a module given its name and returns a handle to it."""
|
||||
try:
|
||||
__import__(module_name)
|
||||
except ImportError as e:
|
||||
error = f"No module named {module_name}"
|
||||
raise ImportError(error) from e
|
||||
return sys.modules[module_name]
|
||||
|
||||
|
||||
def load_class(dotted_path: str) -> type[Trick]:
|
||||
"""Loads and returns a class definition provided a dotted path
|
||||
specification the last part of the dotted path is the class name
|
||||
and there is at least one module name preceding the class name.
|
||||
|
||||
Notes
|
||||
-----
|
||||
You will need to ensure that the module you are trying to load
|
||||
exists in the Python path.
|
||||
|
||||
Examples
|
||||
--------
|
||||
- module.name.ClassName # Provided module.name is in the Python path.
|
||||
- module.ClassName # Provided module is in the Python path.
|
||||
|
||||
What won't work:
|
||||
- ClassName
|
||||
- modle.name.ClassName # Typo in module name.
|
||||
- module.name.ClasNam # Typo in classname.
|
||||
|
||||
"""
|
||||
dotted_path_split = dotted_path.split(".")
|
||||
if len(dotted_path_split) <= 1:
|
||||
error = f"Dotted module path {dotted_path} must contain a module name and a classname"
|
||||
raise ValueError(error)
|
||||
klass_name = dotted_path_split[-1]
|
||||
module_name = ".".join(dotted_path_split[:-1])
|
||||
|
||||
module = load_module(module_name)
|
||||
if hasattr(module, klass_name):
|
||||
return getattr(module, klass_name)
|
||||
|
||||
error = f"Module {module_name} does not have class attribute {klass_name}"
|
||||
raise AttributeError(error)
|
||||
90
myenv/lib/python3.11/site-packages/watchdog/utils/bricks.py
Normal file
90
myenv/lib/python3.11/site-packages/watchdog/utils/bricks.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Utility collections or "bricks".
|
||||
|
||||
:module: watchdog.utils.bricks
|
||||
:author: yesudeep@google.com (Yesudeep Mangalapilly)
|
||||
:author: lalinsky@gmail.com (Lukáš Lalinský)
|
||||
:author: python@rcn.com (Raymond Hettinger)
|
||||
:author: contact@tiger-222.fr (Mickaël Schoentgen)
|
||||
|
||||
Classes
|
||||
=======
|
||||
.. autoclass:: OrderedSetQueue
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: OrderedSet
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SkipRepeatsQueue(queue.Queue):
|
||||
"""Thread-safe implementation of an special queue where a
|
||||
put of the last-item put'd will be dropped.
|
||||
|
||||
The implementation leverages locking already implemented in the base class
|
||||
redefining only the primitives.
|
||||
|
||||
Queued items must be immutable and hashable so that they can be used
|
||||
as dictionary keys. You must implement **only read-only properties** and
|
||||
the :meth:`Item.__hash__()`, :meth:`Item.__eq__()`, and
|
||||
:meth:`Item.__ne__()` methods for items to be hashable.
|
||||
|
||||
An example implementation follows::
|
||||
|
||||
class Item:
|
||||
def __init__(self, a, b):
|
||||
self._a = a
|
||||
self._b = b
|
||||
|
||||
@property
|
||||
def a(self):
|
||||
return self._a
|
||||
|
||||
@property
|
||||
def b(self):
|
||||
return self._b
|
||||
|
||||
def _key(self):
|
||||
return (self._a, self._b)
|
||||
|
||||
def __eq__(self, item):
|
||||
return self._key() == item._key()
|
||||
|
||||
def __ne__(self, item):
|
||||
return self._key() != item._key()
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._key())
|
||||
|
||||
based on the OrderedSetQueue below
|
||||
"""
|
||||
|
||||
def _init(self, maxsize: int) -> None:
|
||||
super()._init(maxsize)
|
||||
self._last_item = None
|
||||
|
||||
def put(self, item: Any, block: bool = True, timeout: float | None = None) -> None: # noqa: FBT001,FBT002
|
||||
"""This method will be used by `eventlet`, when enabled, so we cannot use force proper keyword-only
|
||||
arguments nor touch the signature. Also, the `timeout` argument will be ignored in that case.
|
||||
"""
|
||||
if self._last_item is None or item != self._last_item:
|
||||
super().put(item, block, timeout)
|
||||
|
||||
def _put(self, item: Any) -> None:
|
||||
super()._put(item)
|
||||
self._last_item = item
|
||||
|
||||
def _get(self) -> Any:
|
||||
item = super()._get()
|
||||
if item is self._last_item:
|
||||
self._last_item = None
|
||||
return item
|
||||
@@ -0,0 +1,77 @@
|
||||
""":module: watchdog.utils.delayed_queue
|
||||
:author: thomas.amland@gmail.com (Thomas Amland)
|
||||
:author: contact@tiger-222.fr (Mickaël Schoentgen)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class DelayedQueue(Generic[T]):
|
||||
def __init__(self, delay: float) -> None:
|
||||
self.delay_sec = delay
|
||||
self._lock = threading.Lock()
|
||||
self._not_empty = threading.Condition(self._lock)
|
||||
self._queue: deque[tuple[T, float, bool]] = deque()
|
||||
self._closed = False
|
||||
|
||||
def put(self, element: T, *, delay: bool = False) -> None:
|
||||
"""Add element to queue."""
|
||||
self._lock.acquire()
|
||||
self._queue.append((element, time.time(), delay))
|
||||
self._not_empty.notify()
|
||||
self._lock.release()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close queue, indicating no more items will be added."""
|
||||
self._closed = True
|
||||
# Interrupt the blocking _not_empty.wait() call in get
|
||||
self._not_empty.acquire()
|
||||
self._not_empty.notify()
|
||||
self._not_empty.release()
|
||||
|
||||
def get(self) -> T | None:
|
||||
"""Remove and return an element from the queue, or this queue has been
|
||||
closed raise the Closed exception.
|
||||
"""
|
||||
while True:
|
||||
# wait for element to be added to queue
|
||||
self._not_empty.acquire()
|
||||
while len(self._queue) == 0 and not self._closed:
|
||||
self._not_empty.wait()
|
||||
|
||||
if self._closed:
|
||||
self._not_empty.release()
|
||||
return None
|
||||
head, insert_time, delay = self._queue[0]
|
||||
self._not_empty.release()
|
||||
|
||||
# wait for delay if required
|
||||
if delay:
|
||||
time_left = insert_time + self.delay_sec - time.time()
|
||||
while time_left > 0:
|
||||
time.sleep(time_left)
|
||||
time_left = insert_time + self.delay_sec - time.time()
|
||||
|
||||
# return element if it's still in the queue
|
||||
with self._lock:
|
||||
if len(self._queue) > 0 and self._queue[0][0] is head:
|
||||
self._queue.popleft()
|
||||
return head
|
||||
|
||||
def remove(self, predicate: Callable[[T], bool]) -> T | None:
|
||||
"""Remove and return the first items for which predicate is True,
|
||||
ignoring delay.
|
||||
"""
|
||||
with self._lock:
|
||||
for i, (elem, *_) in enumerate(self._queue):
|
||||
if predicate(elem):
|
||||
del self._queue[i]
|
||||
return elem
|
||||
return None
|
||||
424
myenv/lib/python3.11/site-packages/watchdog/utils/dirsnapshot.py
Normal file
424
myenv/lib/python3.11/site-packages/watchdog/utils/dirsnapshot.py
Normal file
@@ -0,0 +1,424 @@
|
||||
""":module: watchdog.utils.dirsnapshot
|
||||
:synopsis: Directory snapshots and comparison.
|
||||
:author: yesudeep@google.com (Yesudeep Mangalapilly)
|
||||
:author: contact@tiger-222.fr (Mickaël Schoentgen)
|
||||
|
||||
.. ADMONITION:: Where are the moved events? They "disappeared"
|
||||
|
||||
This implementation does not take partition boundaries
|
||||
into consideration. It will only work when the directory
|
||||
tree is entirely on the same file system. More specifically,
|
||||
any part of the code that depends on inode numbers can
|
||||
break if partition boundaries are crossed. In these cases,
|
||||
the snapshot diff will represent file/directory movement as
|
||||
created and deleted events.
|
||||
|
||||
Classes
|
||||
-------
|
||||
.. autoclass:: DirectorySnapshot
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: DirectorySnapshotDiff
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: EmptyDirectorySnapshot
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import os
|
||||
from stat import S_ISDIR
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class DirectorySnapshotDiff:
|
||||
"""Compares two directory snapshots and creates an object that represents
|
||||
the difference between the two snapshots.
|
||||
|
||||
:param ref:
|
||||
The reference directory snapshot.
|
||||
:type ref:
|
||||
:class:`DirectorySnapshot`
|
||||
:param snapshot:
|
||||
The directory snapshot which will be compared
|
||||
with the reference snapshot.
|
||||
:type snapshot:
|
||||
:class:`DirectorySnapshot`
|
||||
:param ignore_device:
|
||||
A boolean indicating whether to ignore the device id or not.
|
||||
By default, a file may be uniquely identified by a combination of its first
|
||||
inode and its device id. The problem is that the device id may (or may not)
|
||||
change between system boots. This problem would cause the DirectorySnapshotDiff
|
||||
to think a file has been deleted and created again but it would be the
|
||||
exact same file.
|
||||
Set to True only if you are sure you will always use the same device.
|
||||
:type ignore_device:
|
||||
:class:`bool`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ref: DirectorySnapshot,
|
||||
snapshot: DirectorySnapshot,
|
||||
*,
|
||||
ignore_device: bool = False,
|
||||
) -> None:
|
||||
created = snapshot.paths - ref.paths
|
||||
deleted = ref.paths - snapshot.paths
|
||||
|
||||
if ignore_device:
|
||||
|
||||
def get_inode(directory: DirectorySnapshot, full_path: bytes | str) -> int | tuple[int, int]:
|
||||
return directory.inode(full_path)[0]
|
||||
|
||||
else:
|
||||
|
||||
def get_inode(directory: DirectorySnapshot, full_path: bytes | str) -> int | tuple[int, int]:
|
||||
return directory.inode(full_path)
|
||||
|
||||
# check that all unchanged paths have the same inode
|
||||
for path in ref.paths & snapshot.paths:
|
||||
if get_inode(ref, path) != get_inode(snapshot, path):
|
||||
created.add(path)
|
||||
deleted.add(path)
|
||||
|
||||
# find moved paths
|
||||
moved: set[tuple[bytes | str, bytes | str]] = set()
|
||||
for path in set(deleted):
|
||||
inode = ref.inode(path)
|
||||
new_path = snapshot.path(inode)
|
||||
if new_path:
|
||||
# file is not deleted but moved
|
||||
deleted.remove(path)
|
||||
moved.add((path, new_path))
|
||||
|
||||
for path in set(created):
|
||||
inode = snapshot.inode(path)
|
||||
old_path = ref.path(inode)
|
||||
if old_path:
|
||||
created.remove(path)
|
||||
moved.add((old_path, path))
|
||||
|
||||
# find modified paths
|
||||
# first check paths that have not moved
|
||||
modified: set[bytes | str] = set()
|
||||
for path in ref.paths & snapshot.paths:
|
||||
if get_inode(ref, path) == get_inode(snapshot, path) and (
|
||||
ref.mtime(path) != snapshot.mtime(path) or ref.size(path) != snapshot.size(path)
|
||||
):
|
||||
modified.add(path)
|
||||
|
||||
for old_path, new_path in moved:
|
||||
if ref.mtime(old_path) != snapshot.mtime(new_path) or ref.size(old_path) != snapshot.size(new_path):
|
||||
modified.add(old_path)
|
||||
|
||||
self._dirs_created = [path for path in created if snapshot.isdir(path)]
|
||||
self._dirs_deleted = [path for path in deleted if ref.isdir(path)]
|
||||
self._dirs_modified = [path for path in modified if ref.isdir(path)]
|
||||
self._dirs_moved = [(frm, to) for (frm, to) in moved if ref.isdir(frm)]
|
||||
|
||||
self._files_created = list(created - set(self._dirs_created))
|
||||
self._files_deleted = list(deleted - set(self._dirs_deleted))
|
||||
self._files_modified = list(modified - set(self._dirs_modified))
|
||||
self._files_moved = list(moved - set(self._dirs_moved))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
fmt = (
|
||||
"<{0} files(created={1}, deleted={2}, modified={3}, moved={4}),"
|
||||
" folders(created={5}, deleted={6}, modified={7}, moved={8})>"
|
||||
)
|
||||
return fmt.format(
|
||||
type(self).__name__,
|
||||
len(self._files_created),
|
||||
len(self._files_deleted),
|
||||
len(self._files_modified),
|
||||
len(self._files_moved),
|
||||
len(self._dirs_created),
|
||||
len(self._dirs_deleted),
|
||||
len(self._dirs_modified),
|
||||
len(self._dirs_moved),
|
||||
)
|
||||
|
||||
@property
|
||||
def files_created(self) -> list[bytes | str]:
|
||||
"""List of files that were created."""
|
||||
return self._files_created
|
||||
|
||||
@property
|
||||
def files_deleted(self) -> list[bytes | str]:
|
||||
"""List of files that were deleted."""
|
||||
return self._files_deleted
|
||||
|
||||
@property
|
||||
def files_modified(self) -> list[bytes | str]:
|
||||
"""List of files that were modified."""
|
||||
return self._files_modified
|
||||
|
||||
@property
|
||||
def files_moved(self) -> list[tuple[bytes | str, bytes | str]]:
|
||||
"""List of files that were moved.
|
||||
|
||||
Each event is a two-tuple the first item of which is the path
|
||||
that has been renamed to the second item in the tuple.
|
||||
"""
|
||||
return self._files_moved
|
||||
|
||||
@property
|
||||
def dirs_modified(self) -> list[bytes | str]:
|
||||
"""List of directories that were modified."""
|
||||
return self._dirs_modified
|
||||
|
||||
@property
|
||||
def dirs_moved(self) -> list[tuple[bytes | str, bytes | str]]:
|
||||
"""List of directories that were moved.
|
||||
|
||||
Each event is a two-tuple the first item of which is the path
|
||||
that has been renamed to the second item in the tuple.
|
||||
"""
|
||||
return self._dirs_moved
|
||||
|
||||
@property
|
||||
def dirs_deleted(self) -> list[bytes | str]:
|
||||
"""List of directories that were deleted."""
|
||||
return self._dirs_deleted
|
||||
|
||||
@property
|
||||
def dirs_created(self) -> list[bytes | str]:
|
||||
"""List of directories that were created."""
|
||||
return self._dirs_created
|
||||
|
||||
class ContextManager:
|
||||
"""Context manager that creates two directory snapshots and a
|
||||
diff object that represents the difference between the two snapshots.
|
||||
|
||||
:param path:
|
||||
The directory path for which a snapshot should be taken.
|
||||
:type path:
|
||||
``str``
|
||||
:param recursive:
|
||||
``True`` if the entire directory tree should be included in the
|
||||
snapshot; ``False`` otherwise.
|
||||
:type recursive:
|
||||
``bool``
|
||||
:param stat:
|
||||
Use custom stat function that returns a stat structure for path.
|
||||
Currently only st_dev, st_ino, st_mode and st_mtime are needed.
|
||||
|
||||
A function taking a ``path`` as argument which will be called
|
||||
for every entry in the directory tree.
|
||||
:param listdir:
|
||||
Use custom listdir function. For details see ``os.scandir``.
|
||||
:param ignore_device:
|
||||
A boolean indicating whether to ignore the device id or not.
|
||||
By default, a file may be uniquely identified by a combination of its first
|
||||
inode and its device id. The problem is that the device id may (or may not)
|
||||
change between system boots. This problem would cause the DirectorySnapshotDiff
|
||||
to think a file has been deleted and created again but it would be the
|
||||
exact same file.
|
||||
Set to True only if you are sure you will always use the same device.
|
||||
:type ignore_device:
|
||||
:class:`bool`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
recursive: bool = True,
|
||||
stat: Callable[[str], os.stat_result] = os.stat,
|
||||
listdir: Callable[[str | None], Iterator[os.DirEntry]] = os.scandir,
|
||||
ignore_device: bool = False,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.recursive = recursive
|
||||
self.stat = stat
|
||||
self.listdir = listdir
|
||||
self.ignore_device = ignore_device
|
||||
|
||||
def __enter__(self) -> None:
|
||||
self.pre_snapshot = self.get_snapshot()
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.post_snapshot = self.get_snapshot()
|
||||
self.diff = DirectorySnapshotDiff(
|
||||
self.pre_snapshot,
|
||||
self.post_snapshot,
|
||||
ignore_device=self.ignore_device,
|
||||
)
|
||||
|
||||
def get_snapshot(self) -> DirectorySnapshot:
|
||||
return DirectorySnapshot(
|
||||
path=self.path,
|
||||
recursive=self.recursive,
|
||||
stat=self.stat,
|
||||
listdir=self.listdir,
|
||||
)
|
||||
|
||||
|
||||
class DirectorySnapshot:
|
||||
"""A snapshot of stat information of files in a directory.
|
||||
|
||||
:param path:
|
||||
The directory path for which a snapshot should be taken.
|
||||
:type path:
|
||||
``str``
|
||||
:param recursive:
|
||||
``True`` if the entire directory tree should be included in the
|
||||
snapshot; ``False`` otherwise.
|
||||
:type recursive:
|
||||
``bool``
|
||||
:param stat:
|
||||
Use custom stat function that returns a stat structure for path.
|
||||
Currently only st_dev, st_ino, st_mode and st_mtime are needed.
|
||||
|
||||
A function taking a ``path`` as argument which will be called
|
||||
for every entry in the directory tree.
|
||||
:param listdir:
|
||||
Use custom listdir function. For details see ``os.scandir``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
recursive: bool = True,
|
||||
stat: Callable[[str], os.stat_result] = os.stat,
|
||||
listdir: Callable[[str | None], Iterator[os.DirEntry]] = os.scandir,
|
||||
) -> None:
|
||||
self.recursive = recursive
|
||||
self.stat = stat
|
||||
self.listdir = listdir
|
||||
|
||||
self._stat_info: dict[bytes | str, os.stat_result] = {}
|
||||
self._inode_to_path: dict[tuple[int, int], bytes | str] = {}
|
||||
|
||||
st = self.stat(path)
|
||||
self._stat_info[path] = st
|
||||
self._inode_to_path[(st.st_ino, st.st_dev)] = path
|
||||
|
||||
for p, st in self.walk(path):
|
||||
i = (st.st_ino, st.st_dev)
|
||||
self._inode_to_path[i] = p
|
||||
self._stat_info[p] = st
|
||||
|
||||
def walk(self, root: str) -> Iterator[tuple[str, os.stat_result]]:
|
||||
try:
|
||||
paths = [os.path.join(root, entry.name) for entry in self.listdir(root)]
|
||||
except OSError as e:
|
||||
# Directory may have been deleted between finding it in the directory
|
||||
# list of its parent and trying to delete its contents. If this
|
||||
# happens we treat it as empty. Likewise if the directory was replaced
|
||||
# with a file of the same name (less likely, but possible).
|
||||
if e.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
entries = []
|
||||
for p in paths:
|
||||
with contextlib.suppress(OSError):
|
||||
entry = (p, self.stat(p))
|
||||
entries.append(entry)
|
||||
yield entry
|
||||
|
||||
if self.recursive:
|
||||
for path, st in entries:
|
||||
with contextlib.suppress(PermissionError):
|
||||
if S_ISDIR(st.st_mode):
|
||||
yield from self.walk(path)
|
||||
|
||||
@property
|
||||
def paths(self) -> set[bytes | str]:
|
||||
"""Set of file/directory paths in the snapshot."""
|
||||
return set(self._stat_info.keys())
|
||||
|
||||
def path(self, uid: tuple[int, int]) -> bytes | str | None:
|
||||
"""Returns path for id. None if id is unknown to this snapshot."""
|
||||
return self._inode_to_path.get(uid)
|
||||
|
||||
def inode(self, path: bytes | str) -> tuple[int, int]:
|
||||
"""Returns an id for path."""
|
||||
st = self._stat_info[path]
|
||||
return (st.st_ino, st.st_dev)
|
||||
|
||||
def isdir(self, path: bytes | str) -> bool:
|
||||
return S_ISDIR(self._stat_info[path].st_mode)
|
||||
|
||||
def mtime(self, path: bytes | str) -> float:
|
||||
return self._stat_info[path].st_mtime
|
||||
|
||||
def size(self, path: bytes | str) -> int:
|
||||
return self._stat_info[path].st_size
|
||||
|
||||
def stat_info(self, path: bytes | str) -> os.stat_result:
|
||||
"""Returns a stat information object for the specified path from
|
||||
the snapshot.
|
||||
|
||||
Attached information is subject to change. Do not use unless
|
||||
you specify `stat` in constructor. Use :func:`inode`, :func:`mtime`,
|
||||
:func:`isdir` instead.
|
||||
|
||||
:param path:
|
||||
The path for which stat information should be obtained
|
||||
from a snapshot.
|
||||
"""
|
||||
return self._stat_info[path]
|
||||
|
||||
def __sub__(self, previous_dirsnap: DirectorySnapshot) -> DirectorySnapshotDiff:
|
||||
"""Allow subtracting a DirectorySnapshot object instance from
|
||||
another.
|
||||
|
||||
:returns:
|
||||
A :class:`DirectorySnapshotDiff` object.
|
||||
"""
|
||||
return DirectorySnapshotDiff(previous_dirsnap, self)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self._stat_info)
|
||||
|
||||
|
||||
class EmptyDirectorySnapshot(DirectorySnapshot):
|
||||
"""Class to implement an empty snapshot. This is used together with
|
||||
DirectorySnapshot and DirectorySnapshotDiff in order to get all the files/folders
|
||||
in the directory as created.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def path(_: Any) -> None:
|
||||
"""Mock up method to return the path of the received inode. As the snapshot
|
||||
is intended to be empty, it always returns None.
|
||||
|
||||
:returns:
|
||||
None.
|
||||
"""
|
||||
return
|
||||
|
||||
@property
|
||||
def paths(self) -> set:
|
||||
"""Mock up method to return a set of file/directory paths in the snapshot. As
|
||||
the snapshot is intended to be empty, it always returns an empty set.
|
||||
|
||||
:returns:
|
||||
An empty set.
|
||||
"""
|
||||
return set()
|
||||
68
myenv/lib/python3.11/site-packages/watchdog/utils/echo.py
Normal file
68
myenv/lib/python3.11/site-packages/watchdog/utils/echo.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# echo.py: Tracing function calls using Python decorators.
|
||||
#
|
||||
# Written by Thomas Guest <tag@wordaligned.org>
|
||||
# Please see http://wordaligned.org/articles/echo
|
||||
#
|
||||
# Place into the public domain.
|
||||
|
||||
"""Echo calls made to functions in a module.
|
||||
|
||||
"Echoing" a function call means printing out the name of the function
|
||||
and the values of its arguments before making the call (which is more
|
||||
commonly referred to as "tracing", but Python already has a trace module).
|
||||
|
||||
Alternatively, echo.echo can be used to decorate functions. Calls to the
|
||||
decorated function will be echoed.
|
||||
|
||||
Example:
|
||||
-------
|
||||
|
||||
@echo.echo
|
||||
def my_function(args):
|
||||
pass
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def format_arg_value(arg_val: tuple[str, tuple[Any, ...]]) -> str:
|
||||
"""Return a string representing a (name, value) pair."""
|
||||
arg, val = arg_val
|
||||
return f"{arg}={val!r}"
|
||||
|
||||
|
||||
def echo(fn: Callable, write: Callable[[str], int | None] = sys.stdout.write) -> Callable:
|
||||
"""Echo calls to a function.
|
||||
|
||||
Returns a decorated version of the input function which "echoes" calls
|
||||
made to it by writing out the function's name and the arguments it was
|
||||
called with.
|
||||
"""
|
||||
# Unpack function's arg count, arg names, arg defaults
|
||||
code = fn.__code__
|
||||
argcount = code.co_argcount
|
||||
argnames = code.co_varnames[:argcount]
|
||||
fn_defaults: tuple[Any] = fn.__defaults__ or ()
|
||||
argdefs = dict(list(zip(argnames[-len(fn_defaults) :], fn_defaults)))
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapped(*v: Any, **k: Any) -> Callable:
|
||||
# Collect function arguments by chaining together positional,
|
||||
# defaulted, extra positional and keyword arguments.
|
||||
positional = list(map(format_arg_value, list(zip(argnames, v))))
|
||||
defaulted = [format_arg_value((a, argdefs[a])) for a in argnames[len(v) :] if a not in k]
|
||||
nameless = list(map(repr, v[argcount:]))
|
||||
keyword = list(map(format_arg_value, list(k.items())))
|
||||
args = positional + defaulted + nameless + keyword
|
||||
write(f"{fn.__name__}({', '.join(args)})\n")
|
||||
return fn(*v, **k)
|
||||
|
||||
return wrapped
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from watchdog.utils import BaseThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
from watchdog.events import FileSystemEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventDebouncer(BaseThread):
|
||||
"""Background thread for debouncing event handling.
|
||||
|
||||
When an event is received, wait until the configured debounce interval
|
||||
passes before calling the callback. If additional events are received
|
||||
before the interval passes, reset the timer and keep waiting. When the
|
||||
debouncing interval passes, the callback will be called with a list of
|
||||
events in the order in which they were received.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
debounce_interval_seconds: int,
|
||||
events_callback: Callable[[list[FileSystemEvent]], None],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.debounce_interval_seconds = debounce_interval_seconds
|
||||
self.events_callback = events_callback
|
||||
|
||||
self._events: list[FileSystemEvent] = []
|
||||
self._cond = threading.Condition()
|
||||
|
||||
def handle_event(self, event: FileSystemEvent) -> None:
|
||||
with self._cond:
|
||||
self._events.append(event)
|
||||
self._cond.notify()
|
||||
|
||||
def stop(self) -> None:
|
||||
with self._cond:
|
||||
super().stop()
|
||||
self._cond.notify()
|
||||
|
||||
def run(self) -> None:
|
||||
with self._cond:
|
||||
while True:
|
||||
# Wait for first event (or shutdown).
|
||||
self._cond.wait()
|
||||
|
||||
if self.debounce_interval_seconds:
|
||||
# Wait for additional events (or shutdown) until the debounce interval passes.
|
||||
while self.should_keep_running():
|
||||
if not self._cond.wait(timeout=self.debounce_interval_seconds):
|
||||
break
|
||||
|
||||
if not self.should_keep_running():
|
||||
break
|
||||
|
||||
events = self._events
|
||||
self._events = []
|
||||
self.events_callback(events)
|
||||
@@ -0,0 +1,99 @@
|
||||
""":module: watchdog.utils.patterns
|
||||
:synopsis: Common wildcard searching/filtering functionality for files.
|
||||
:author: boris.staletic@gmail.com (Boris Staletic)
|
||||
:author: yesudeep@gmail.com (Yesudeep Mangalapilly)
|
||||
:author: contact@tiger-222.fr (Mickaël Schoentgen)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Non-pure path objects are only allowed on their respective OS's.
|
||||
# Thus, these utilities require "pure" path objects that don't access the filesystem.
|
||||
# Since pathlib doesn't have a `case_sensitive` parameter, we have to approximate it
|
||||
# by converting input paths to `PureWindowsPath` and `PurePosixPath` where:
|
||||
# - `PureWindowsPath` is always case-insensitive.
|
||||
# - `PurePosixPath` is always case-sensitive.
|
||||
# Reference: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match
|
||||
from pathlib import PurePosixPath, PureWindowsPath
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
def _match_path(
|
||||
raw_path: str,
|
||||
included_patterns: set[str],
|
||||
excluded_patterns: set[str],
|
||||
*,
|
||||
case_sensitive: bool,
|
||||
) -> bool:
|
||||
"""Internal function same as :func:`match_path` but does not check arguments."""
|
||||
path: PurePosixPath | PureWindowsPath
|
||||
if case_sensitive:
|
||||
path = PurePosixPath(raw_path)
|
||||
else:
|
||||
included_patterns = {pattern.lower() for pattern in included_patterns}
|
||||
excluded_patterns = {pattern.lower() for pattern in excluded_patterns}
|
||||
path = PureWindowsPath(raw_path)
|
||||
|
||||
common_patterns = included_patterns & excluded_patterns
|
||||
if common_patterns:
|
||||
error = f"conflicting patterns `{common_patterns}` included and excluded"
|
||||
raise ValueError(error)
|
||||
|
||||
return any(path.match(p) for p in included_patterns) and not any(path.match(p) for p in excluded_patterns)
|
||||
|
||||
|
||||
def filter_paths(
|
||||
paths: list[str],
|
||||
*,
|
||||
included_patterns: list[str] | None = None,
|
||||
excluded_patterns: list[str] | None = None,
|
||||
case_sensitive: bool = True,
|
||||
) -> Iterator[str]:
|
||||
"""Filters from a set of paths based on acceptable patterns and
|
||||
ignorable patterns.
|
||||
:param paths:
|
||||
A list of path names that will be filtered based on matching and
|
||||
ignored patterns.
|
||||
:param included_patterns:
|
||||
Allow filenames matching wildcard patterns specified in this list.
|
||||
If no pattern list is specified, ["*"] is used as the default pattern,
|
||||
which matches all files.
|
||||
:param excluded_patterns:
|
||||
Ignores filenames matching wildcard patterns specified in this list.
|
||||
If no pattern list is specified, no files are ignored.
|
||||
:param case_sensitive:
|
||||
``True`` if matching should be case-sensitive; ``False`` otherwise.
|
||||
:returns:
|
||||
A list of pathnames that matched the allowable patterns and passed
|
||||
through the ignored patterns.
|
||||
"""
|
||||
included = set(["*"] if included_patterns is None else included_patterns)
|
||||
excluded = set([] if excluded_patterns is None else excluded_patterns)
|
||||
|
||||
for path in paths:
|
||||
if _match_path(path, included, excluded, case_sensitive=case_sensitive):
|
||||
yield path
|
||||
|
||||
|
||||
def match_any_paths(
|
||||
paths: list[str],
|
||||
*,
|
||||
included_patterns: list[str] | None = None,
|
||||
excluded_patterns: list[str] | None = None,
|
||||
case_sensitive: bool = True,
|
||||
) -> bool:
|
||||
"""Matches from a set of paths based on acceptable patterns and
|
||||
ignorable patterns.
|
||||
See ``filter_paths()`` for signature details.
|
||||
"""
|
||||
return any(
|
||||
filter_paths(
|
||||
paths,
|
||||
included_patterns=included_patterns,
|
||||
excluded_patterns=excluded_patterns,
|
||||
case_sensitive=case_sensitive,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
PLATFORM_WINDOWS = "windows"
|
||||
PLATFORM_LINUX = "linux"
|
||||
PLATFORM_BSD = "bsd"
|
||||
PLATFORM_DARWIN = "darwin"
|
||||
PLATFORM_UNKNOWN = "unknown"
|
||||
|
||||
|
||||
def get_platform_name() -> str:
|
||||
if sys.platform.startswith("win"):
|
||||
return PLATFORM_WINDOWS
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
return PLATFORM_DARWIN
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
return PLATFORM_LINUX
|
||||
|
||||
if sys.platform.startswith(("dragonfly", "freebsd", "netbsd", "openbsd", "bsd")):
|
||||
return PLATFORM_BSD
|
||||
|
||||
return PLATFORM_UNKNOWN
|
||||
|
||||
|
||||
__platform__ = get_platform_name()
|
||||
|
||||
|
||||
def is_linux() -> bool:
|
||||
return __platform__ == PLATFORM_LINUX
|
||||
|
||||
|
||||
def is_bsd() -> bool:
|
||||
return __platform__ == PLATFORM_BSD
|
||||
|
||||
|
||||
def is_darwin() -> bool:
|
||||
return __platform__ == PLATFORM_DARWIN
|
||||
|
||||
|
||||
def is_windows() -> bool:
|
||||
return __platform__ == PLATFORM_WINDOWS
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from watchdog.utils import BaseThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import subprocess
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProcessWatcher(BaseThread):
|
||||
def __init__(self, popen_obj: subprocess.Popen, process_termination_callback: Callable[[], None] | None) -> None:
|
||||
super().__init__()
|
||||
self.popen_obj = popen_obj
|
||||
self.process_termination_callback = process_termination_callback
|
||||
|
||||
def run(self) -> None:
|
||||
while self.popen_obj.poll() is None:
|
||||
if self.stopped_event.wait(timeout=0.1):
|
||||
return
|
||||
|
||||
try:
|
||||
if not self.stopped_event.is_set() and self.process_termination_callback:
|
||||
self.process_termination_callback()
|
||||
except Exception:
|
||||
logger.exception("Error calling process termination callback")
|
||||
Reference in New Issue
Block a user