syncwave 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {syncwave-0.1.0 → syncwave-0.1.2}/PKG-INFO +4 -4
- {syncwave-0.1.0 → syncwave-0.1.2}/README.md +3 -2
- {syncwave-0.1.0 → syncwave-0.1.2}/pyproject.toml +8 -5
- {syncwave-0.1.0 → syncwave-0.1.2}/src/syncwave/__init__.py +1 -1
- {syncwave-0.1.0 → syncwave-0.1.2}/src/syncwave/io.py +5 -7
- {syncwave-0.1.0 → syncwave-0.1.2}/src/syncwave/syncwave.py +6 -6
- syncwave-0.1.2/src/syncwave/watcher.py +177 -0
- syncwave-0.1.0/src/syncwave/watcher.py +0 -139
- {syncwave-0.1.0 → syncwave-0.1.2}/LICENSE +0 -0
- {syncwave-0.1.0 → syncwave-0.1.2}/src/syncwave/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: syncwave
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects.
|
|
5
5
|
Keywords:
|
|
6
6
|
Author: Lucas Iannitello
|
|
@@ -29,7 +29,6 @@ License: MIT License
|
|
|
29
29
|
Requires-Dist: pydantic>=2.11.7
|
|
30
30
|
Requires-Dist: watchdog>=6.0.0
|
|
31
31
|
Requires-Python: >=3.9
|
|
32
|
-
Project-URL: Documentation, https://github.com/iannitello/syncwave
|
|
33
32
|
Project-URL: Homepage, https://github.com/iannitello/syncwave
|
|
34
33
|
Project-URL: Issues, https://github.com/iannitello/syncwave/issues
|
|
35
34
|
Project-URL: Repository, https://github.com/iannitello/syncwave
|
|
@@ -39,14 +38,15 @@ Description-Content-Type: text/markdown
|
|
|
39
38
|
|
|
40
39
|
Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects.
|
|
41
40
|
|
|
42
|
-
>
|
|
41
|
+
> ⚠️ **Warning**
|
|
42
|
+
>
|
|
43
43
|
> Syncwave is under active development. Until version **1.0**, any minor release (`0.x`) may introduce breaking API changes. Pin the exact version to use in production.
|
|
44
44
|
>
|
|
45
45
|
> Version **1.0** will be released when the library is stable, feature-complete, and tested.
|
|
46
46
|
|
|
47
47
|
## Getting Started
|
|
48
48
|
|
|
49
|
-
Install from [PyPI](https://pypi.org):
|
|
49
|
+
Install from [PyPI](https://pypi.org/project/syncwave/):
|
|
50
50
|
|
|
51
51
|
```shell
|
|
52
52
|
# pip
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> ⚠️ **Warning**
|
|
6
|
+
>
|
|
6
7
|
> Syncwave is under active development. Until version **1.0**, any minor release (`0.x`) may introduce breaking API changes. Pin the exact version to use in production.
|
|
7
8
|
>
|
|
8
9
|
> Version **1.0** will be released when the library is stable, feature-complete, and tested.
|
|
9
10
|
|
|
10
11
|
## Getting Started
|
|
11
12
|
|
|
12
|
-
Install from [PyPI](https://pypi.org):
|
|
13
|
+
Install from [PyPI](https://pypi.org/project/syncwave/):
|
|
13
14
|
|
|
14
15
|
```shell
|
|
15
16
|
# pip
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "syncwave"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Lucas Iannitello", email = "lucas.iannitello@protonmail.com" }]
|
|
@@ -15,9 +15,8 @@ classifiers = []
|
|
|
15
15
|
|
|
16
16
|
[project.urls]
|
|
17
17
|
Homepage = "https://github.com/iannitello/syncwave"
|
|
18
|
-
Documentation = "https://github.com/iannitello/syncwave"
|
|
19
|
-
Repository = "https://github.com/iannitello/syncwave"
|
|
20
18
|
Issues = "https://github.com/iannitello/syncwave/issues"
|
|
19
|
+
Repository = "https://github.com/iannitello/syncwave"
|
|
21
20
|
|
|
22
21
|
[build-system]
|
|
23
22
|
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
@@ -25,8 +24,12 @@ build-backend = "uv_build"
|
|
|
25
24
|
|
|
26
25
|
[dependency-groups]
|
|
27
26
|
dev = [
|
|
28
|
-
"ruff>=0.12.
|
|
27
|
+
"ruff>=0.12.9",
|
|
29
28
|
]
|
|
30
29
|
|
|
31
30
|
[tool.ruff.lint]
|
|
32
|
-
|
|
31
|
+
select = ["E", "F"]
|
|
32
|
+
|
|
33
|
+
unfixable = [
|
|
34
|
+
"F401" # disable fix for unused imports
|
|
35
|
+
]
|
|
@@ -62,15 +62,13 @@ class _IO:
|
|
|
62
62
|
timer.start()
|
|
63
63
|
|
|
64
64
|
def _scheduled_write(self, path: Path, data_provider: DataProvider) -> None:
|
|
65
|
-
|
|
66
|
-
self.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self._debounce_timers.pop(path, None)
|
|
70
|
-
self._pending_data_providers.pop(path, None)
|
|
65
|
+
with self._lock:
|
|
66
|
+
self._debounce_timers.pop(path, None)
|
|
67
|
+
self._pending_data_providers.pop(path, None)
|
|
68
|
+
self._atomic_write(path, data_provider())
|
|
71
69
|
|
|
72
70
|
def _atomic_write(self, path: Path, data: JSONData) -> None:
|
|
73
|
-
fd, tmp_path = mkstemp(prefix=
|
|
71
|
+
fd, tmp_path = mkstemp(prefix=watcher.TMP_FILE_PREFIX, dir=path.parent)
|
|
74
72
|
try:
|
|
75
73
|
with os.fdopen(fd, "w", encoding=self.ENCODING) as tmp_file:
|
|
76
74
|
json.dump(data, tmp_file, **self.DUMPS_CONFIG)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import threading
|
|
3
1
|
from collections.abc import Mapping
|
|
4
2
|
from inspect import isclass
|
|
3
|
+
from json import JSONDecodeError
|
|
5
4
|
from pathlib import Path
|
|
5
|
+
from threading import RLock
|
|
6
6
|
from typing import Any, Callable, Iterator, Optional, Type, Union, get_args
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ValidationError
|
|
@@ -62,7 +62,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
62
62
|
self.path = self._get_path(file_name or name_, sub_dir, file_path)
|
|
63
63
|
|
|
64
64
|
self._ssd: dict[JSONKey, BaseModel] = {} # actual mapping, ssd = SyncStoreDict
|
|
65
|
-
self._lock =
|
|
65
|
+
self._lock = RLock()
|
|
66
66
|
self._patch_cls()
|
|
67
67
|
io.init_json_file(self.path)
|
|
68
68
|
self.load()
|
|
@@ -111,9 +111,9 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
111
111
|
# no JSONDecodeError or ValidationError at this point, safe to clear
|
|
112
112
|
self._ssd.clear()
|
|
113
113
|
self._ssd.update(tmp_ssd)
|
|
114
|
-
except (
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
except (FileNotFoundError, JSONDecodeError, ValidationError):
|
|
115
|
+
pass
|
|
116
|
+
finally:
|
|
117
117
|
io.write_json(self.path, self.to_dict)
|
|
118
118
|
|
|
119
119
|
def _get_path(
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from threading import Lock, Timer
|
|
3
|
+
from time import monotonic, sleep
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from watchdog.events import (
|
|
7
|
+
DirDeletedEvent,
|
|
8
|
+
DirMovedEvent,
|
|
9
|
+
FileSystemEvent,
|
|
10
|
+
FileSystemEventHandler,
|
|
11
|
+
)
|
|
12
|
+
from watchdog.observers import Observer
|
|
13
|
+
from watchdog.observers.api import ObservedWatch
|
|
14
|
+
|
|
15
|
+
DirPath = Path
|
|
16
|
+
FilePath = Path
|
|
17
|
+
Callback = Callable[[], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Watcher:
|
|
21
|
+
TMP_FILE_PREFIX = ".tmp_"
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._lock = Lock()
|
|
25
|
+
# dir_path -> (watch object, set of file_paths)
|
|
26
|
+
self._watched_dirs: dict[DirPath, tuple[ObservedWatch, set[FilePath]]] = {}
|
|
27
|
+
|
|
28
|
+
self._observer = Observer()
|
|
29
|
+
self._event_handler = _EventHandler(self)
|
|
30
|
+
self._observer.start()
|
|
31
|
+
|
|
32
|
+
def watch(self, file_path: FilePath, callback: Callback) -> None:
|
|
33
|
+
if not file_path.is_file():
|
|
34
|
+
raise ValueError(f"Path '{file_path}' is not a file.")
|
|
35
|
+
|
|
36
|
+
dir_path = file_path.parent
|
|
37
|
+
with self._lock:
|
|
38
|
+
# if the directory is already being watched, just add the file to the set
|
|
39
|
+
if dir_path in self._watched_dirs:
|
|
40
|
+
_, watched_files = self._watched_dirs[dir_path]
|
|
41
|
+
watched_files.add(file_path)
|
|
42
|
+
# otherwise, schedule the directory and create the set of watched files
|
|
43
|
+
else:
|
|
44
|
+
self._watched_dirs[dir_path] = (
|
|
45
|
+
self._observer.schedule(self._event_handler, str(dir_path)),
|
|
46
|
+
{file_path},
|
|
47
|
+
)
|
|
48
|
+
# set or reset the callback in all cases
|
|
49
|
+
self._event_handler.set_callback(file_path, callback)
|
|
50
|
+
|
|
51
|
+
def unwatch(self, file_path: FilePath) -> None:
|
|
52
|
+
# remove the callback
|
|
53
|
+
self._event_handler.unset_callback(file_path)
|
|
54
|
+
|
|
55
|
+
dir_path = file_path.parent
|
|
56
|
+
with self._lock:
|
|
57
|
+
# if the directory is not being watched, do nothing
|
|
58
|
+
if dir_path not in self._watched_dirs:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# if the file is in the set, remove it
|
|
62
|
+
watch, watched_files = self._watched_dirs[dir_path]
|
|
63
|
+
if file_path in watched_files:
|
|
64
|
+
watched_files.remove(file_path)
|
|
65
|
+
|
|
66
|
+
# if the set is empty, unschedule the directory
|
|
67
|
+
if not watched_files:
|
|
68
|
+
self._observer.unschedule(watch)
|
|
69
|
+
del self._watched_dirs[dir_path]
|
|
70
|
+
|
|
71
|
+
def mark_self_write(self, file_path: FilePath) -> None:
|
|
72
|
+
self._event_handler.mark_self_write(file_path)
|
|
73
|
+
|
|
74
|
+
def get_watched_dirs(self) -> dict[DirPath, set[FilePath]]:
|
|
75
|
+
with self._lock:
|
|
76
|
+
return {
|
|
77
|
+
dir_path: watched_files.copy()
|
|
78
|
+
for dir_path, (_, watched_files) in self._watched_dirs.items()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def restore_watched_dir(self, dir_path: DirPath) -> None:
|
|
82
|
+
with self._lock:
|
|
83
|
+
if dir_path not in self._watched_dirs:
|
|
84
|
+
return
|
|
85
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
watch, watched_files = self._watched_dirs[dir_path]
|
|
87
|
+
self._observer.unschedule(watch)
|
|
88
|
+
self._watched_dirs[dir_path] = (
|
|
89
|
+
self._observer.schedule(self._event_handler, str(dir_path)),
|
|
90
|
+
watched_files,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def stop(self) -> None:
|
|
94
|
+
self._observer.stop()
|
|
95
|
+
self._observer.join()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _EventHandler(FileSystemEventHandler):
|
|
99
|
+
DEBOUNCE_WINDOW = 0.05
|
|
100
|
+
SELF_WRITE_WINDOW = 0.5
|
|
101
|
+
|
|
102
|
+
def __init__(self, watcher: _Watcher) -> None:
|
|
103
|
+
self._watcher = watcher
|
|
104
|
+
self._lock = Lock()
|
|
105
|
+
self._callbacks: dict[FilePath, Callback] = {}
|
|
106
|
+
self._debounce_timers: dict[FilePath, Timer] = {}
|
|
107
|
+
self._self_writes_ts: dict[FilePath, float] = {}
|
|
108
|
+
|
|
109
|
+
def set_callback(self, file_path: FilePath, callback: Callback) -> None:
|
|
110
|
+
with self._lock:
|
|
111
|
+
self._callbacks[file_path] = callback
|
|
112
|
+
|
|
113
|
+
def unset_callback(self, file_path: FilePath) -> None:
|
|
114
|
+
with self._lock:
|
|
115
|
+
self._callbacks.pop(file_path, None)
|
|
116
|
+
|
|
117
|
+
def mark_self_write(self, file_path: FilePath) -> None:
|
|
118
|
+
with self._lock:
|
|
119
|
+
self._self_writes_ts[file_path] = monotonic()
|
|
120
|
+
|
|
121
|
+
def _is_self_write(self, file_path: FilePath) -> bool:
|
|
122
|
+
# not protected by the lock because it's only called internally
|
|
123
|
+
ts = self._self_writes_ts.get(file_path)
|
|
124
|
+
if ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW:
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def dispatch(self, event: FileSystemEvent) -> None:
|
|
129
|
+
if event.is_directory:
|
|
130
|
+
if not isinstance(event, (DirDeletedEvent, DirMovedEvent)):
|
|
131
|
+
return
|
|
132
|
+
watched_dirs = self._watcher.get_watched_dirs()
|
|
133
|
+
for dir_path in self._paths_from_event(event):
|
|
134
|
+
if dir_path in watched_dirs:
|
|
135
|
+
self._on_dir_deleted(dir_path, watched_dirs[dir_path])
|
|
136
|
+
else:
|
|
137
|
+
for file_path in self._paths_from_event(event):
|
|
138
|
+
self._on_file_modified(file_path)
|
|
139
|
+
|
|
140
|
+
def _on_dir_deleted(self, dir_path: DirPath, watched_files: set[FilePath]) -> None:
|
|
141
|
+
sleep(self.DEBOUNCE_WINDOW) # low-tech debounce
|
|
142
|
+
self._watcher.restore_watched_dir(dir_path)
|
|
143
|
+
for file_path in watched_files:
|
|
144
|
+
self._on_file_modified(file_path)
|
|
145
|
+
|
|
146
|
+
def _on_file_modified(self, file_path: FilePath) -> None:
|
|
147
|
+
with self._lock:
|
|
148
|
+
if file_path not in self._callbacks or self._is_self_write(file_path):
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if file_path in self._debounce_timers:
|
|
152
|
+
self._debounce_timers[file_path].cancel()
|
|
153
|
+
|
|
154
|
+
timer = Timer(
|
|
155
|
+
self.DEBOUNCE_WINDOW,
|
|
156
|
+
self._scheduled_callback,
|
|
157
|
+
args=(file_path, self._callbacks[file_path]),
|
|
158
|
+
)
|
|
159
|
+
self._debounce_timers[file_path] = timer
|
|
160
|
+
timer.start()
|
|
161
|
+
|
|
162
|
+
def _scheduled_callback(self, file_path: FilePath, callback: Callback) -> None:
|
|
163
|
+
with self._lock:
|
|
164
|
+
self._debounce_timers.pop(file_path, None)
|
|
165
|
+
callback()
|
|
166
|
+
|
|
167
|
+
def _paths_from_event(self, event: FileSystemEvent) -> list[Path]:
|
|
168
|
+
paths = []
|
|
169
|
+
if self._watcher.TMP_FILE_PREFIX not in event.src_path:
|
|
170
|
+
paths.append(Path(event.src_path).resolve())
|
|
171
|
+
dest_path = getattr(event, "dest_path", "")
|
|
172
|
+
if dest_path and self._watcher.TMP_FILE_PREFIX not in dest_path:
|
|
173
|
+
paths.append(Path(dest_path).resolve())
|
|
174
|
+
return paths
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
watcher = _Watcher()
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from threading import Lock, Timer
|
|
3
|
-
from time import monotonic
|
|
4
|
-
from typing import Callable, Union
|
|
5
|
-
|
|
6
|
-
from watchdog.events import (
|
|
7
|
-
DirModifiedEvent,
|
|
8
|
-
FileModifiedEvent,
|
|
9
|
-
FileSystemEvent,
|
|
10
|
-
FileSystemEventHandler,
|
|
11
|
-
)
|
|
12
|
-
from watchdog.observers import Observer
|
|
13
|
-
from watchdog.observers.api import ObservedWatch
|
|
14
|
-
|
|
15
|
-
DirPath = Path
|
|
16
|
-
FilePath = Path
|
|
17
|
-
Callback = Callable[[], None]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class _Watcher:
|
|
21
|
-
SELF_WRITE_WINDOW = 0.5
|
|
22
|
-
|
|
23
|
-
def __init__(self) -> None:
|
|
24
|
-
self._lock = Lock()
|
|
25
|
-
# mapping dir_path -> (watch object, set of file_paths)
|
|
26
|
-
self._watched_dirs: dict[DirPath, tuple[ObservedWatch, set[FilePath]]] = {}
|
|
27
|
-
self._self_writes_ts: dict[FilePath, float] = {}
|
|
28
|
-
self._callbacks: dict[FilePath, Callback] = {}
|
|
29
|
-
|
|
30
|
-
self._observer = Observer()
|
|
31
|
-
self._event_handler = _EventHandler(self)
|
|
32
|
-
self._observer.start()
|
|
33
|
-
|
|
34
|
-
def stop(self) -> None:
|
|
35
|
-
self._observer.stop()
|
|
36
|
-
self._observer.join()
|
|
37
|
-
|
|
38
|
-
def watch(self, file_path: FilePath, callback: Callback) -> None:
|
|
39
|
-
dir_path = file_path.parent
|
|
40
|
-
|
|
41
|
-
if not file_path.is_file():
|
|
42
|
-
raise ValueError(f"Path {file_path} is not a file.")
|
|
43
|
-
|
|
44
|
-
with self._lock:
|
|
45
|
-
# if the directory is already being watched...
|
|
46
|
-
if dir_path in self._watched_dirs:
|
|
47
|
-
_, watched_files = self._watched_dirs[dir_path]
|
|
48
|
-
|
|
49
|
-
# ...but the file is not in the set, add it
|
|
50
|
-
if file_path not in watched_files:
|
|
51
|
-
watched_files.add(file_path)
|
|
52
|
-
|
|
53
|
-
# and in any case, update the callback before returning
|
|
54
|
-
self._callbacks[file_path] = callback
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
# otherwise, schedule the directory and register the callback
|
|
58
|
-
self._watched_dirs[dir_path] = (
|
|
59
|
-
self._observer.schedule(self._event_handler, str(dir_path)),
|
|
60
|
-
{file_path},
|
|
61
|
-
)
|
|
62
|
-
self._callbacks[file_path] = callback
|
|
63
|
-
|
|
64
|
-
def unwatch(self, file_path: FilePath) -> None:
|
|
65
|
-
dir_path = file_path.parent
|
|
66
|
-
|
|
67
|
-
with self._lock:
|
|
68
|
-
# remove the callback
|
|
69
|
-
self._callbacks.pop(file_path, None)
|
|
70
|
-
|
|
71
|
-
# if the directory is not being watched, do nothing
|
|
72
|
-
if dir_path not in self._watched_dirs:
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
watch, watched_files = self._watched_dirs[dir_path]
|
|
76
|
-
|
|
77
|
-
# if the file is in the set, remove it
|
|
78
|
-
if file_path in watched_files:
|
|
79
|
-
watched_files.remove(file_path)
|
|
80
|
-
|
|
81
|
-
# if the set is empty, unschedule the directory
|
|
82
|
-
if not watched_files:
|
|
83
|
-
self._observer.unschedule(watch)
|
|
84
|
-
del self._watched_dirs[dir_path]
|
|
85
|
-
|
|
86
|
-
def mark_self_write(self, file_path: FilePath) -> None:
|
|
87
|
-
with self._lock:
|
|
88
|
-
self._self_writes_ts[file_path] = monotonic()
|
|
89
|
-
|
|
90
|
-
def _is_self_write(self, file_path: FilePath) -> bool:
|
|
91
|
-
with self._lock:
|
|
92
|
-
ts = self._self_writes_ts.get(file_path)
|
|
93
|
-
if ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW:
|
|
94
|
-
return True
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class _EventHandler(FileSystemEventHandler):
|
|
99
|
-
DEBOUNCE_WINDOW = 0.05
|
|
100
|
-
|
|
101
|
-
def __init__(self, watcher: _Watcher) -> None:
|
|
102
|
-
self.lock = Lock()
|
|
103
|
-
self.watcher = watcher
|
|
104
|
-
self.debounce_timers: dict[FileSystemEvent, Timer] = {}
|
|
105
|
-
|
|
106
|
-
def on_modified(self, event: Union[FileModifiedEvent, DirModifiedEvent]) -> None:
|
|
107
|
-
if event.is_directory or isinstance(event, DirModifiedEvent):
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
file_path = Path(event.src_path).resolve()
|
|
111
|
-
|
|
112
|
-
if self.watcher._is_self_write(file_path):
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
callback = self.watcher._callbacks.get(file_path)
|
|
116
|
-
if callback is None:
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
with self.lock:
|
|
120
|
-
if event in self.debounce_timers:
|
|
121
|
-
self.debounce_timers[event].cancel()
|
|
122
|
-
|
|
123
|
-
timer = Timer(
|
|
124
|
-
self.DEBOUNCE_WINDOW,
|
|
125
|
-
self.schedule_callback,
|
|
126
|
-
args=(event, callback),
|
|
127
|
-
)
|
|
128
|
-
self.debounce_timers[event] = timer
|
|
129
|
-
timer.start()
|
|
130
|
-
|
|
131
|
-
def schedule_callback(self, event: FileSystemEvent, callback: Callback) -> None:
|
|
132
|
-
try:
|
|
133
|
-
callback()
|
|
134
|
-
finally:
|
|
135
|
-
with self.lock:
|
|
136
|
-
self.debounce_timers.pop(event, None)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
watcher = _Watcher()
|
|
File without changes
|
|
File without changes
|