robotframework-filewatcher 0.1.0__py3-none-any.whl
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.
- FileWatcher/__init__.py +4 -0
- FileWatcher/exceptions.py +18 -0
- FileWatcher/keywords/waiting.py +628 -0
- FileWatcher/keywords/watching.py +132 -0
- FileWatcher/library.py +52 -0
- FileWatcher/models.py +119 -0
- FileWatcher/watcher.py +343 -0
- robotframework_filewatcher-0.1.0.dist-info/METADATA +253 -0
- robotframework_filewatcher-0.1.0.dist-info/RECORD +12 -0
- robotframework_filewatcher-0.1.0.dist-info/WHEEL +5 -0
- robotframework_filewatcher-0.1.0.dist-info/licenses/LICENSE +201 -0
- robotframework_filewatcher-0.1.0.dist-info/top_level.txt +1 -0
FileWatcher/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class FileWatcherError(Exception):
|
|
2
|
+
"""Base exception for all FileWatcher library errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileWatcherTimeoutError(FileWatcherError):
|
|
7
|
+
"""Raised when a wait operation times out waiting for a file system event."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DirectoryNotWatchedError(FileWatcherError):
|
|
12
|
+
"""Raised when trying to perform an action on a directory that is not currently being watched."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DirectoryAlreadyWatchedError(FileWatcherError):
|
|
17
|
+
"""Raised when trying to watch a directory that is already registered and watched."""
|
|
18
|
+
pass
|
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import time
|
|
3
|
+
from robot.api.deco import keyword
|
|
4
|
+
from FileWatcher.exceptions import FileWatcherTimeoutError, FileWatcherError
|
|
5
|
+
from FileWatcher.models import EventType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WaitingKeywords:
|
|
9
|
+
"""Keywords for waiting for file events and checking stability."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, ctx: any) -> None:
|
|
12
|
+
"""Initializes the WaitingKeywords component.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
ctx: The parent FileWatcherLibrary context.
|
|
16
|
+
"""
|
|
17
|
+
self.ctx = ctx
|
|
18
|
+
|
|
19
|
+
@keyword("Wait For File Created")
|
|
20
|
+
def wait_for_file_created(
|
|
21
|
+
self, pattern: str | None = None, since_id: int = 0, timeout: float = 30.0
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""Wait until a creation event matching a pattern appears.
|
|
24
|
+
|
|
25
|
+
This keyword blocks until an event of type 'created' that matches the
|
|
26
|
+
optional glob `pattern` is observed in the EventStore or the
|
|
27
|
+
`timeout` is reached.
|
|
28
|
+
|
|
29
|
+
Arguments:
|
|
30
|
+
pattern: Optional glob pattern (for example "*.pdf"). If omitted,
|
|
31
|
+
any created file will match.
|
|
32
|
+
since_id: Only consider events with ID greater than this value.
|
|
33
|
+
timeout: Maximum seconds to wait before raising a timeout error.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
dict: A dictionary representing the matched event. Typical keys
|
|
37
|
+
include 'id', 'event_type', 'src_path', 'dest_path',
|
|
38
|
+
'watched_directory', and 'timestamp'.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileWatcherTimeoutError: When no matching event appears before timeout.
|
|
42
|
+
|
|
43
|
+
*Examples*
|
|
44
|
+
| ***** Settings *****
|
|
45
|
+
| Library FileWatcher
|
|
46
|
+
|
|
|
47
|
+
| ***** Test Cases *****
|
|
48
|
+
| Example
|
|
49
|
+
| ${event} Wait For File Created *.pdf
|
|
50
|
+
| ${event} Wait For File Created report.xlsx since_id=12 timeout=15
|
|
51
|
+
"""
|
|
52
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
53
|
+
event = store.wait_for_event(
|
|
54
|
+
event_type=EventType.CREATED,
|
|
55
|
+
pattern=pattern,
|
|
56
|
+
since_id=int(since_id),
|
|
57
|
+
timeout=float(timeout),
|
|
58
|
+
)
|
|
59
|
+
return event.to_dict()
|
|
60
|
+
|
|
61
|
+
@keyword("Wait For File Modified")
|
|
62
|
+
def wait_for_file_modified(
|
|
63
|
+
self, pattern: str | None = None, since_id: int = 0, timeout: float = 30.0
|
|
64
|
+
) -> dict:
|
|
65
|
+
"""Wait until a file modification event matching `pattern` appears.
|
|
66
|
+
|
|
67
|
+
Arguments:
|
|
68
|
+
pattern: Optional glob pattern to filter modified files.
|
|
69
|
+
since_id: Only consider events with ID greater than this value.
|
|
70
|
+
timeout: Maximum seconds to wait.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
dict: Dictionary representation of the matched modification event.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
FileWatcherTimeoutError: When no matching event appears before timeout.
|
|
77
|
+
|
|
78
|
+
*Examples*
|
|
79
|
+
| ***** Settings *****
|
|
80
|
+
| Library FileWatcher
|
|
81
|
+
|
|
|
82
|
+
| ***** Test Cases *****
|
|
83
|
+
| Example
|
|
84
|
+
| ${event} Wait For File Modified *.pdf
|
|
85
|
+
"""
|
|
86
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
87
|
+
event = store.wait_for_event(
|
|
88
|
+
event_type=EventType.MODIFIED,
|
|
89
|
+
pattern=pattern,
|
|
90
|
+
since_id=int(since_id),
|
|
91
|
+
timeout=float(timeout),
|
|
92
|
+
)
|
|
93
|
+
return event.to_dict()
|
|
94
|
+
|
|
95
|
+
@keyword("Wait For File Deleted")
|
|
96
|
+
def wait_for_file_deleted(
|
|
97
|
+
self, pattern: str | None = None, since_id: int = 0, timeout: float = 30.0
|
|
98
|
+
) -> dict:
|
|
99
|
+
"""Wait until a file deletion event matching `pattern` appears.
|
|
100
|
+
|
|
101
|
+
Arguments:
|
|
102
|
+
pattern: Optional glob pattern to filter deleted files.
|
|
103
|
+
since_id: Only consider events with ID greater than this value.
|
|
104
|
+
timeout: Maximum seconds to wait.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict: Dictionary representation of the matched deletion event.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
FileWatcherTimeoutError: When no matching event appears before timeout.
|
|
111
|
+
|
|
112
|
+
*Examples*
|
|
113
|
+
| ***** Settings *****
|
|
114
|
+
| Library FileWatcher
|
|
115
|
+
|
|
|
116
|
+
| ***** Test Cases *****
|
|
117
|
+
| Example
|
|
118
|
+
| ${event} Wait For File Deleted *.pdf
|
|
119
|
+
"""
|
|
120
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
121
|
+
event = store.wait_for_event(
|
|
122
|
+
event_type=EventType.DELETED,
|
|
123
|
+
pattern=pattern,
|
|
124
|
+
since_id=int(since_id),
|
|
125
|
+
timeout=float(timeout),
|
|
126
|
+
)
|
|
127
|
+
return event.to_dict()
|
|
128
|
+
|
|
129
|
+
@keyword("Wait Until File Stable")
|
|
130
|
+
def wait_until_file_stable(
|
|
131
|
+
self, pattern: str, stability_time: float = 2.0, timeout: float = 30.0
|
|
132
|
+
) -> dict:
|
|
133
|
+
"""Wait until a file matching `pattern` has stabilized.
|
|
134
|
+
|
|
135
|
+
Resolution strategy:
|
|
136
|
+
1. Check EventStore for recent events matching `pattern`.
|
|
137
|
+
2. Scan watched directories on disk.
|
|
138
|
+
3. Block and wait for a new matching event if not found.
|
|
139
|
+
|
|
140
|
+
After resolving a candidate file, the function watches for both
|
|
141
|
+
filesystem size changes and new events for `stability_time` seconds
|
|
142
|
+
to consider the file stable.
|
|
143
|
+
|
|
144
|
+
Arguments:
|
|
145
|
+
pattern: Glob pattern to identify the file (required).
|
|
146
|
+
stability_time: Seconds the file must remain unchanged.
|
|
147
|
+
timeout: Maximum seconds to wait before raising a timeout.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
dict: Event dictionary representing the stable file event.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
FileWatcherTimeoutError: If file cannot be resolved or does not
|
|
154
|
+
stabilize before `timeout`.
|
|
155
|
+
|
|
156
|
+
*Examples*
|
|
157
|
+
| ***** Settings *****
|
|
158
|
+
| Library FileWatcher
|
|
159
|
+
|
|
|
160
|
+
| ***** Test Cases *****
|
|
161
|
+
| Example
|
|
162
|
+
| ${event} Wait Until File Stable *.pdf stability_time=5 timeout=60
|
|
163
|
+
"""
|
|
164
|
+
stability_time = float(stability_time)
|
|
165
|
+
timeout = float(timeout)
|
|
166
|
+
deadline = time.time() + timeout
|
|
167
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
168
|
+
|
|
169
|
+
matched_path: Path | None = None
|
|
170
|
+
|
|
171
|
+
# 1. EventStore-first: Check recorded history for any events matching the pattern
|
|
172
|
+
matching_events = [
|
|
173
|
+
e for e in store.get_all() if e.matches_glob(pattern)
|
|
174
|
+
]
|
|
175
|
+
if matching_events:
|
|
176
|
+
latest_event = matching_events[-1]
|
|
177
|
+
matched_path = (
|
|
178
|
+
latest_event.src_path
|
|
179
|
+
if latest_event.event_type != EventType.MOVED
|
|
180
|
+
else latest_event.dest_path
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 2. Filesystem-second: Scan watched directories on disk
|
|
184
|
+
if matched_path is None:
|
|
185
|
+
for watched_dir in self.ctx.watch_manager.watched_directories():
|
|
186
|
+
try:
|
|
187
|
+
for p in watched_dir.rglob(pattern):
|
|
188
|
+
if p.is_file():
|
|
189
|
+
matched_path = p
|
|
190
|
+
break
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
if matched_path is not None:
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
# 3. Block and wait for a new event to arrive if not resolved
|
|
197
|
+
if matched_path is None:
|
|
198
|
+
remaining = deadline - time.time()
|
|
199
|
+
if remaining <= 0:
|
|
200
|
+
raise FileWatcherTimeoutError(
|
|
201
|
+
f"Timed out waiting for file matching pattern '{pattern}' to exist."
|
|
202
|
+
)
|
|
203
|
+
event = store.wait_for_event(pattern=pattern, timeout=remaining)
|
|
204
|
+
matched_path = (
|
|
205
|
+
event.src_path if event.event_type != EventType.MOVED else event.dest_path
|
|
206
|
+
)
|
|
207
|
+
assert matched_path is not None
|
|
208
|
+
|
|
209
|
+
# 4. Once path is resolved, verify stability
|
|
210
|
+
last_check_time = time.time()
|
|
211
|
+
last_size = None
|
|
212
|
+
if matched_path.exists():
|
|
213
|
+
last_size = matched_path.stat().st_size
|
|
214
|
+
|
|
215
|
+
while True:
|
|
216
|
+
remaining = deadline - time.time()
|
|
217
|
+
if remaining <= 0:
|
|
218
|
+
raise FileWatcherTimeoutError(
|
|
219
|
+
f"Timed out waiting for file '{matched_path}' to stabilize."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Sleep for stability_time or remaining time
|
|
223
|
+
sleep_time = min(stability_time, remaining)
|
|
224
|
+
time.sleep(sleep_time)
|
|
225
|
+
|
|
226
|
+
if not matched_path.exists():
|
|
227
|
+
raise FileWatcherTimeoutError(
|
|
228
|
+
f"File '{matched_path}' was deleted or does not exist during stability check."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
current_size = matched_path.stat().st_size
|
|
232
|
+
|
|
233
|
+
# Check if any new events were recorded for this file
|
|
234
|
+
new_events = [
|
|
235
|
+
e
|
|
236
|
+
for e in store.get_all()
|
|
237
|
+
if e.timestamp > last_check_time
|
|
238
|
+
and (e.src_path == matched_path or e.dest_path == matched_path)
|
|
239
|
+
and e.event_type
|
|
240
|
+
in (EventType.MODIFIED, EventType.MOVED, EventType.CREATED)
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
if len(new_events) > 0 or current_size != last_size:
|
|
244
|
+
# File is still active. Reset timer.
|
|
245
|
+
last_size = current_size
|
|
246
|
+
last_check_time = time.time()
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# No changes during the interval. It is stable!
|
|
250
|
+
# Find the last matching event in the store to return
|
|
251
|
+
all_file_events = [
|
|
252
|
+
e
|
|
253
|
+
for e in store.get_all()
|
|
254
|
+
if (e.src_path == matched_path or e.dest_path == matched_path)
|
|
255
|
+
]
|
|
256
|
+
if all_file_events:
|
|
257
|
+
return all_file_events[-1].to_dict()
|
|
258
|
+
else:
|
|
259
|
+
# Fallback dictionary if it existed pre-watch and no events are recorded
|
|
260
|
+
return {
|
|
261
|
+
"id": 0,
|
|
262
|
+
"event_type": "stable",
|
|
263
|
+
"src_path": str(matched_path),
|
|
264
|
+
"dest_path": None,
|
|
265
|
+
"watched_directory": "",
|
|
266
|
+
"timestamp": time.time(),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@keyword("Get File Events")
|
|
270
|
+
def get_file_events(self) -> list[dict]:
|
|
271
|
+
"""Return all events currently retained in the EventStore.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
list[dict]: A list of event dictionaries ordered by increasing ID.
|
|
275
|
+
|
|
276
|
+
*Examples*
|
|
277
|
+
| ***** Settings *****
|
|
278
|
+
| Library FileWatcher
|
|
279
|
+
|
|
|
280
|
+
| ***** Test Cases *****
|
|
281
|
+
| Example
|
|
282
|
+
| ${events} Get File Events
|
|
283
|
+
"""
|
|
284
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
285
|
+
return [e.to_dict() for e in store.get_all()]
|
|
286
|
+
|
|
287
|
+
@keyword("Get File Events Since")
|
|
288
|
+
def get_file_events_since(self, event_id: int) -> list[dict]:
|
|
289
|
+
"""Return events whose ID is strictly greater than `event_id`.
|
|
290
|
+
|
|
291
|
+
Arguments:
|
|
292
|
+
event_id: Integer event ID to compare.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
list[dict]: Matching events with ID > event_id.
|
|
296
|
+
|
|
297
|
+
*Examples*
|
|
298
|
+
| ***** Settings *****
|
|
299
|
+
| Library FileWatcher
|
|
300
|
+
|
|
|
301
|
+
| ***** Test Cases *****
|
|
302
|
+
| Example
|
|
303
|
+
| ${new_events} Get File Events Since 42
|
|
304
|
+
"""
|
|
305
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
306
|
+
return [e.to_dict() for e in store.get_since(int(event_id))]
|
|
307
|
+
|
|
308
|
+
@keyword("Clear Event History")
|
|
309
|
+
def clear_event_history(self) -> None:
|
|
310
|
+
"""Clear all events from the in-memory EventStore.
|
|
311
|
+
|
|
312
|
+
Use this to reset test state when previous events should not influence
|
|
313
|
+
subsequent assertions or waits.
|
|
314
|
+
|
|
315
|
+
*Examples*
|
|
316
|
+
| ***** Settings *****
|
|
317
|
+
| Library FileWatcher
|
|
318
|
+
|
|
|
319
|
+
| ***** Test Cases *****
|
|
320
|
+
| Example
|
|
321
|
+
| Clear Event History
|
|
322
|
+
"""
|
|
323
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
324
|
+
store.clear()
|
|
325
|
+
|
|
326
|
+
# Commenting this keyword for now since it has a few issues
|
|
327
|
+
# @keyword("Wait For Download")
|
|
328
|
+
def _wait_for_download(
|
|
329
|
+
self, pattern: str, stability_time: float = 2.0, timeout: float = 30.0
|
|
330
|
+
) -> dict:
|
|
331
|
+
"""(Internal) Wait for a download to stabilize.
|
|
332
|
+
|
|
333
|
+
Alias for `Wait Until File Stable`. Kept as a private helper for
|
|
334
|
+
historical reasons and not exported as a Robot keyword.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
dict: The stable FileEvent dictionary.
|
|
338
|
+
|
|
339
|
+
*Examples*
|
|
340
|
+
| ***** Settings *****
|
|
341
|
+
| Library FileWatcher
|
|
342
|
+
|
|
|
343
|
+
| ***** Test Cases *****
|
|
344
|
+
| Example
|
|
345
|
+
| ${event} Wait For Download *.pdf stability_time=5 timeout=60
|
|
346
|
+
"""
|
|
347
|
+
return self.wait_until_file_stable(pattern, stability_time, timeout)
|
|
348
|
+
|
|
349
|
+
@keyword("Wait Until Directory Is Not Empty")
|
|
350
|
+
def wait_until_directory_is_not_empty(
|
|
351
|
+
self, path: str | Path, timeout: float = 30.0
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Block until the directory has at least one file or subdirectory.
|
|
354
|
+
|
|
355
|
+
Arguments:
|
|
356
|
+
path: Directory to monitor.
|
|
357
|
+
timeout: Maximum seconds to wait.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
FileWatcherTimeoutError: If the directory remains empty until timeout.
|
|
361
|
+
|
|
362
|
+
*Examples*
|
|
363
|
+
| ***** Settings *****
|
|
364
|
+
| Library FileWatcher
|
|
365
|
+
|
|
|
366
|
+
| ***** Test Cases *****
|
|
367
|
+
| Example
|
|
368
|
+
| Wait Until Directory Is Not Empty /path/to/directory timeout=15
|
|
369
|
+
"""
|
|
370
|
+
resolved_path = Path(path).resolve()
|
|
371
|
+
if not resolved_path.exists() or not resolved_path.is_dir():
|
|
372
|
+
raise FileWatcherError(f"Directory does not exist: {path}")
|
|
373
|
+
|
|
374
|
+
deadline = time.time() + float(timeout)
|
|
375
|
+
while True:
|
|
376
|
+
if any(resolved_path.iterdir()):
|
|
377
|
+
return
|
|
378
|
+
if time.time() >= deadline:
|
|
379
|
+
raise FileWatcherTimeoutError(
|
|
380
|
+
f"Timed out waiting for directory '{resolved_path}' to become non-empty."
|
|
381
|
+
)
|
|
382
|
+
time.sleep(0.2)
|
|
383
|
+
|
|
384
|
+
@keyword("Wait Until File Count Is")
|
|
385
|
+
def wait_until_file_count_is(
|
|
386
|
+
self,
|
|
387
|
+
path: str | Path,
|
|
388
|
+
count: int,
|
|
389
|
+
pattern: str = "*",
|
|
390
|
+
timeout: float = 30.0,
|
|
391
|
+
) -> bool:
|
|
392
|
+
"""Block until the number of files matching `pattern` equals `count`.
|
|
393
|
+
|
|
394
|
+
Arguments:
|
|
395
|
+
path: Directory to scan.
|
|
396
|
+
count: Expected number of matching files.
|
|
397
|
+
pattern: Glob pattern to count. Defaults to '*'.
|
|
398
|
+
timeout: Maximum seconds to wait.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
bool: True when the count matches.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
FileWatcherTimeoutError: If the count does not match before timeout.
|
|
405
|
+
|
|
406
|
+
*Examples*
|
|
407
|
+
| ***** Settings *****
|
|
408
|
+
| Library FileWatcher
|
|
409
|
+
|
|
|
410
|
+
| ***** Test Cases *****
|
|
411
|
+
| Example
|
|
412
|
+
| Wait Until File Count Is /path/to/directory 5 *.pdf
|
|
413
|
+
"""
|
|
414
|
+
resolved_path = Path(path).resolve()
|
|
415
|
+
if not resolved_path.exists() or not resolved_path.is_dir():
|
|
416
|
+
raise FileWatcherError(f"Directory does not exist: {path}")
|
|
417
|
+
|
|
418
|
+
deadline = time.time() + float(timeout)
|
|
419
|
+
while True:
|
|
420
|
+
current_count = len(list(resolved_path.glob(pattern)))
|
|
421
|
+
if current_count == int(count):
|
|
422
|
+
return True
|
|
423
|
+
if time.time() >= deadline:
|
|
424
|
+
raise FileWatcherTimeoutError(
|
|
425
|
+
f"Timed out waiting for {count} files matching '{pattern}' in '{resolved_path}'."
|
|
426
|
+
)
|
|
427
|
+
time.sleep(0.2)
|
|
428
|
+
|
|
429
|
+
@keyword("Find Files Matching Pattern")
|
|
430
|
+
def find_files_matching_pattern(
|
|
431
|
+
self, pattern: str, recursive: bool = True
|
|
432
|
+
) -> list[str]:
|
|
433
|
+
"""Search watched directories for files matching `pattern`.
|
|
434
|
+
|
|
435
|
+
Arguments:
|
|
436
|
+
pattern: Glob pattern to search for.
|
|
437
|
+
recursive: Whether to search recursively in subdirectories.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
list[str]: Sorted unique absolute file paths matching the pattern.
|
|
441
|
+
|
|
442
|
+
*Examples*
|
|
443
|
+
| ***** Settings *****
|
|
444
|
+
| Library FileWatcher
|
|
445
|
+
|
|
|
446
|
+
| ***** Test Cases *****
|
|
447
|
+
| Example
|
|
448
|
+
| @{files} Find Files Matching Pattern *.pdf
|
|
449
|
+
| Log Found @{files}
|
|
450
|
+
"""
|
|
451
|
+
wm = self.ctx.watch_manager
|
|
452
|
+
files: list[str] = []
|
|
453
|
+
seen: set[str] = set()
|
|
454
|
+
|
|
455
|
+
with wm._lock:
|
|
456
|
+
for watched_dir in wm.watched_directories():
|
|
457
|
+
try:
|
|
458
|
+
iterator = (
|
|
459
|
+
watched_dir.rglob(pattern)
|
|
460
|
+
if recursive
|
|
461
|
+
else watched_dir.glob(pattern)
|
|
462
|
+
)
|
|
463
|
+
for p in iterator:
|
|
464
|
+
if p.is_file():
|
|
465
|
+
absolute_path = str(p.resolve())
|
|
466
|
+
if absolute_path not in seen:
|
|
467
|
+
seen.add(absolute_path)
|
|
468
|
+
files.append(absolute_path)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
return sorted(files)
|
|
473
|
+
|
|
474
|
+
@keyword("Get File Count")
|
|
475
|
+
def get_file_count(
|
|
476
|
+
self, pattern: str = "*", recursive: bool = True
|
|
477
|
+
) -> int:
|
|
478
|
+
"""Return the number of files matching `pattern` across watched dirs.
|
|
479
|
+
|
|
480
|
+
Arguments:
|
|
481
|
+
pattern: Glob pattern to count. Defaults to '*'.
|
|
482
|
+
recursive: Whether to search recursively (default True).
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
int: Number of matching files.
|
|
486
|
+
|
|
487
|
+
*Examples*
|
|
488
|
+
| ***** Settings *****
|
|
489
|
+
| Library FileWatcher
|
|
490
|
+
|
|
|
491
|
+
| ***** Test Cases *****
|
|
492
|
+
| Example
|
|
493
|
+
| ${count} Get File Count *.pdf
|
|
494
|
+
| Log Found ${count} PDF files.
|
|
495
|
+
"""
|
|
496
|
+
return len(self.find_files_matching_pattern(pattern, recursive))
|
|
497
|
+
|
|
498
|
+
@keyword("Get Current Event Id")
|
|
499
|
+
def get_current_event_id(self) -> int:
|
|
500
|
+
"""Return the numeric ID of the most recent event in the EventStore.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
int: Maximum event ID currently stored, or 0 when empty.
|
|
504
|
+
|
|
505
|
+
*Examples*
|
|
506
|
+
| ***** Settings *****
|
|
507
|
+
| Library FileWatcher
|
|
508
|
+
|
|
|
509
|
+
| ***** Test Cases *****
|
|
510
|
+
| Example
|
|
511
|
+
| ${current_id} Get Current Event Id
|
|
512
|
+
"""
|
|
513
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
514
|
+
return store.get_current_event_id()
|
|
515
|
+
|
|
516
|
+
@keyword("Get Event Types")
|
|
517
|
+
def get_event_types(self) -> list[str]:
|
|
518
|
+
"""Return the list of supported event type names.
|
|
519
|
+
|
|
520
|
+
This is useful for callers of `Should Have File Event` to know which
|
|
521
|
+
values are accepted for the `event_type` parameter.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
list[str]: ['created', 'modified', 'deleted', 'moved']
|
|
525
|
+
|
|
526
|
+
*Examples*
|
|
527
|
+
| ***** Settings *****
|
|
528
|
+
| Library FileWatcher
|
|
529
|
+
|
|
|
530
|
+
| ***** Test Cases *****
|
|
531
|
+
| Example
|
|
532
|
+
| @{types} Get Event Types
|
|
533
|
+
| Log @{types}
|
|
534
|
+
"""
|
|
535
|
+
from FileWatcher.models import EventType
|
|
536
|
+
|
|
537
|
+
return [e.value for e in EventType]
|
|
538
|
+
|
|
539
|
+
@keyword("Get Latest File")
|
|
540
|
+
def get_latest_file(self, pattern: str | None = None, limit: int = 1) -> str | list[str]:
|
|
541
|
+
"""Searches all watched directories for files matching the pattern.
|
|
542
|
+
|
|
543
|
+
Sorts them by modification time descending and returns the latest file(s).
|
|
544
|
+
|
|
545
|
+
*Examples*
|
|
546
|
+
| ***** Settings *****
|
|
547
|
+
| Library FileWatcher
|
|
548
|
+
|
|
|
549
|
+
| ***** Test Cases *****
|
|
550
|
+
| Example
|
|
551
|
+
| @{latest_files} Get Latest File *.pdf limit=5
|
|
552
|
+
"""
|
|
553
|
+
limit = int(limit)
|
|
554
|
+
glob_pattern = pattern if pattern else "*"
|
|
555
|
+
wm = self.ctx.watch_manager
|
|
556
|
+
files_found = []
|
|
557
|
+
|
|
558
|
+
with wm._lock:
|
|
559
|
+
for resolved_path, watch in wm._watches.items():
|
|
560
|
+
is_recursive = watch.is_recursive
|
|
561
|
+
try:
|
|
562
|
+
generator = resolved_path.rglob(glob_pattern) if is_recursive else resolved_path.glob(glob_pattern)
|
|
563
|
+
for p in generator:
|
|
564
|
+
try:
|
|
565
|
+
if p.is_file():
|
|
566
|
+
mtime = p.stat().st_mtime
|
|
567
|
+
files_found.append((p, mtime))
|
|
568
|
+
except (OSError, FileNotFoundError):
|
|
569
|
+
pass
|
|
570
|
+
except Exception:
|
|
571
|
+
pass
|
|
572
|
+
|
|
573
|
+
if not files_found:
|
|
574
|
+
raise FileWatcherError(
|
|
575
|
+
f"No files matching pattern '{glob_pattern}' found in watched directories."
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Sort descending by mtime
|
|
579
|
+
files_found.sort(key=lambda x: x[1], reverse=True)
|
|
580
|
+
str_paths = [str(item[0]) for item in files_found]
|
|
581
|
+
|
|
582
|
+
if limit == 1:
|
|
583
|
+
return str_paths[0]
|
|
584
|
+
else:
|
|
585
|
+
return str_paths[:limit]
|
|
586
|
+
|
|
587
|
+
@keyword("Should Have File Event")
|
|
588
|
+
def should_have_file_event(self, event_type: str | None = None, pattern: str | None = None) -> None:
|
|
589
|
+
"""Assert the EventStore contains at least one matching event.
|
|
590
|
+
|
|
591
|
+
Arguments:
|
|
592
|
+
event_type: Optional event type string. Valid values are:
|
|
593
|
+
'created', 'modified', 'deleted', 'moved'. Use
|
|
594
|
+
`Get Event Types` to retrieve the current supported list.
|
|
595
|
+
pattern: Optional glob pattern to filter events.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
AssertionError: When no event matching the filters exists.
|
|
599
|
+
|
|
600
|
+
*Examples*
|
|
601
|
+
| ***** Settings *****
|
|
602
|
+
| Library FileWatcher
|
|
603
|
+
|
|
|
604
|
+
| ***** Test Cases *****
|
|
605
|
+
| Example
|
|
606
|
+
| Should Have File Event event_type=created pattern=*.pdf
|
|
607
|
+
"""
|
|
608
|
+
store = self.ctx.watch_manager.get_event_store()
|
|
609
|
+
events = store.get_all()
|
|
610
|
+
|
|
611
|
+
target_type = None
|
|
612
|
+
if event_type is not None:
|
|
613
|
+
try:
|
|
614
|
+
target_type = EventType(event_type.lower())
|
|
615
|
+
except ValueError:
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
for event in events:
|
|
619
|
+
if target_type is not None and event.event_type != target_type:
|
|
620
|
+
continue
|
|
621
|
+
if pattern is not None and not event.matches_glob(pattern):
|
|
622
|
+
continue
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
raise AssertionError(
|
|
626
|
+
f"Assertion Failed: No event matching (event_type={event_type}, pattern={pattern}) "
|
|
627
|
+
f"was found in the EventStore history (total events: {len(events)})."
|
|
628
|
+
)
|