yhttp 9.4.2__tar.gz → 9.6.0__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.
Files changed (64) hide show
  1. {yhttp-9.4.2/yhttp.egg-info → yhttp-9.6.0}/PKG-INFO +2 -2
  2. {yhttp-9.4.2 → yhttp-9.6.0}/setup.py +1 -1
  3. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_fswatcher.py +31 -0
  4. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_static.py +116 -0
  5. yhttp-9.6.0/yhttp/core/fswatcher.py +145 -0
  6. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/static.py +89 -19
  7. yhttp-9.6.0/yhttp/core/version.py +1 -0
  8. {yhttp-9.4.2 → yhttp-9.6.0/yhttp.egg-info}/PKG-INFO +2 -2
  9. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/requires.txt +1 -1
  10. yhttp-9.4.2/yhttp/core/fswatcher.py +0 -137
  11. yhttp-9.4.2/yhttp/core/version.py +0 -1
  12. {yhttp-9.4.2 → yhttp-9.6.0}/LICENSE +0 -0
  13. {yhttp-9.4.2 → yhttp-9.6.0}/README.md +0 -0
  14. {yhttp-9.4.2 → yhttp-9.6.0}/setup.cfg +0 -0
  15. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_application.py +0 -0
  16. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_applicationcli.py +0 -0
  17. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_serve.py +0 -0
  18. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_version.py +0 -0
  19. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_yhttp_version.py +0 -0
  20. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_cookie.py +0 -0
  21. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_cookieset.py +0 -0
  22. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_extension.py +0 -0
  23. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form.py +0 -0
  24. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_json.py +0 -0
  25. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_multipart.py +0 -0
  26. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_urlencoded.py +0 -0
  27. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard.py +0 -0
  28. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_body.py +0 -0
  29. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_contenttypes.py +0 -0
  30. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_query.py +0 -0
  31. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_headerset.py +0 -0
  32. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_jsonencoding.py +0 -0
  33. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_locale.py +0 -0
  34. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_logging.py +0 -0
  35. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_middleware.py +0 -0
  36. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_multidict.py +0 -0
  37. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_multipart.py +0 -0
  38. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_querystring.py +0 -0
  39. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_request.py +0 -0
  40. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_responseheaders.py +0 -0
  41. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_rewrite.py +0 -0
  42. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_rewritecli.py +0 -0
  43. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_routing.py +0 -0
  44. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_statuses.py +0 -0
  45. {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_statushandler.py +0 -0
  46. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/__init__.py +0 -0
  47. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/application.py +0 -0
  48. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/cli.py +0 -0
  49. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/contenttypes.py +0 -0
  50. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/cookieset.py +0 -0
  51. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/guard.py +0 -0
  52. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/headerset.py +0 -0
  53. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/logging.py +0 -0
  54. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/main.py +0 -0
  55. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/multidict.py +0 -0
  56. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/multipart.py +0 -0
  57. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/request.py +0 -0
  58. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/response.py +0 -0
  59. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/rewrite.py +0 -0
  60. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/statuses.py +0 -0
  61. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/SOURCES.txt +0 -0
  62. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/dependency_links.txt +0 -0
  63. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/entry_points.txt +0 -0
  64. {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yhttp
3
- Version: 9.4.2
3
+ Version: 9.6.0
4
4
  Summary: A very micro http framework.
5
5
  Home-page: http://github.com/yhttp/yhttp
6
6
  Author: Vahid Mardani
@@ -29,7 +29,7 @@ License-File: LICENSE
29
29
  Requires-Dist: snam>=1.0.2
30
30
  Requires-Dist: easycli<2,>=1.10.1
31
31
  Requires-Dist: ujson
32
- Requires-Dist: inotify-simple
32
+ Requires-Dist: watchdog
33
33
  Dynamic: author
34
34
  Dynamic: author-email
35
35
  Dynamic: classifier
@@ -18,7 +18,7 @@ dependencies = [
18
18
  'snam >= 1.0.2',
19
19
  'easycli >= 1.10.1, < 2',
20
20
  'ujson',
21
- 'inotify-simple',
21
+ 'watchdog',
22
22
  ]
23
23
 
24
24
 
@@ -1,3 +1,5 @@
1
+ from types import SimpleNamespace
2
+
1
3
  from yhttp.core.fswatcher import FSWatcher
2
4
 
3
5
 
@@ -50,3 +52,32 @@ def test_watcher(mktmptree, chdir):
50
52
  assert not w.wait()
51
53
 
52
54
  w.close()
55
+
56
+
57
+ def test_moved_event_reports_watched_destination(tmp_path):
58
+ watched_file = tmp_path / 'watched.txt'
59
+ w = FSWatcher(files=[str(watched_file)])
60
+ w._watchfiles.add(str(watched_file))
61
+ event = SimpleNamespace(
62
+ event_type='moved', src_path=str(tmp_path / 'old.txt'),
63
+ dest_path=str(watched_file),
64
+ )
65
+
66
+ assert w._event_paths(event) == {str(watched_file)}
67
+
68
+
69
+ def test_wait_collects_event_until_timeout(monkeypatch, tmp_path):
70
+ watched_file = tmp_path / 'watched.txt'
71
+ w = FSWatcher(files=[str(watched_file)])
72
+ w._watchfiles.add(str(watched_file))
73
+ w._events.put(SimpleNamespace(
74
+ event_type='modified', src_path=str(watched_file),
75
+ ))
76
+ # Start before the deadline, then report that the event arrived after it.
77
+ ticks = iter((0, 0.001, 0.006))
78
+ monkeypatch.setattr(
79
+ 'yhttp.core.fswatcher.time.monotonic',
80
+ lambda: next(ticks)
81
+ )
82
+
83
+ assert w.wait(timeout=5) == {str(watched_file)}
@@ -1,6 +1,11 @@
1
1
  from os import path
2
+ from types import SimpleNamespace
2
3
 
3
4
  from bddrest import status, response, when
5
+ import pytest
6
+
7
+ from yhttp.core.headerset import HeaderSet
8
+ from yhttp.core.static import file as staticfile
4
9
 
5
10
 
6
11
  def test_staticfile(app, httpreq, tmpdir):
@@ -17,6 +22,117 @@ def test_staticfile(app, httpreq, tmpdir):
17
22
  assert response == 'foo'
18
23
 
19
24
 
25
+ def test_staticfile_byte_ranges(app, httpreq, tmpdir):
26
+ filename = path.join(tmpdir, 'range.txt')
27
+ with open(filename, 'wb') as f:
28
+ f.write(b'0123456789')
29
+
30
+ app.staticfile(r'/range\.txt', filename)
31
+
32
+ with httpreq('/range.txt', headers={'Range': 'bytes=2-5'}):
33
+ assert status == 206
34
+ assert response.headers['accept-ranges'] == 'bytes'
35
+ assert response.headers['content-range'] == 'bytes 2-5/10'
36
+ assert response.headers['content-length'] == '4'
37
+ assert response == '2345'
38
+
39
+ with httpreq('/range.txt', headers={'Range': 'bytes=-3'}):
40
+ assert status == 206
41
+ assert response.headers['content-range'] == 'bytes 7-9/10'
42
+ assert response == '789'
43
+
44
+ with httpreq('/range.txt', headers={'Range': 'bytes=20-'}):
45
+ assert status == 416
46
+ assert response.headers['content-range'] == 'bytes */10'
47
+
48
+ with httpreq('/range.txt', headers={'Range': 'bytes=-0'}):
49
+ assert status == 416
50
+ assert response.headers['content-range'] == 'bytes */10'
51
+
52
+ with httpreq('/range.txt', headers={'Range': 'invalid'}):
53
+ assert status == 200
54
+ assert response.headers['content-length'] == '10'
55
+ assert response == '0123456789'
56
+
57
+ with httpreq(
58
+ '/range.txt', headers={'Range': 'bytes=0-1,4-5'}
59
+ ):
60
+ assert status == 200
61
+ assert response == '0123456789'
62
+
63
+
64
+ @pytest.mark.parametrize('range_header', [
65
+ 'bytes=10',
66
+ 'bytes=-',
67
+ 'bytes=3-1',
68
+ 'bytes=abc-def',
69
+ 'bytes=--1',
70
+ ])
71
+ def test_malformed_or_unsupported_ranges_send_full_file(
72
+ app, httpreq, tmpdir, range_header,
73
+ ):
74
+ filename = path.join(tmpdir, 'range.txt')
75
+ with open(filename, 'wb') as f:
76
+ f.write(b'0123456789')
77
+ app.staticfile(r'/range\.txt', filename)
78
+
79
+ with httpreq('/range.txt', headers={'Range': range_header}):
80
+ assert status == 200
81
+ assert response == '0123456789'
82
+
83
+
84
+ def test_empty_static_file(app, httpreq, tmpdir):
85
+ filename = path.join(tmpdir, 'empty.txt')
86
+ open(filename, 'wb').close()
87
+ app.staticfile(r'/empty\.txt', filename)
88
+
89
+ with httpreq('/empty.txt'):
90
+ assert status == 200
91
+ assert response.headers['content-length'] == '0'
92
+ assert response == ''
93
+
94
+
95
+ def test_staticfile_short_read_after_size_lookup(tmpdir):
96
+ filename = path.join(tmpdir, 'short.txt')
97
+ with open(filename, 'wb') as f:
98
+ f.write(b'content')
99
+
100
+ handler = staticfile(filename)
101
+ request = SimpleNamespace(
102
+ verb='get', environ={},
103
+ response=SimpleNamespace(headers=HeaderSet()),
104
+ )
105
+ open(filename, 'wb').close()
106
+
107
+ assert list(handler(request)) == []
108
+
109
+
110
+ def test_staticdirectory_byte_ranges(app, httpreq, tmpdir):
111
+ filename = path.join(tmpdir, 'range.txt')
112
+ with open(filename, 'wb') as f:
113
+ f.write(b'0123456789')
114
+ app.staticdirectory('/static', tmpdir)
115
+
116
+ with httpreq('/static/range.txt', headers={'Range': 'bytes=3-'}):
117
+ assert status == 206
118
+ assert response.headers['content-range'] == 'bytes 3-9/10'
119
+ assert response == '3456789'
120
+
121
+
122
+ def test_range_request_with_if_range_sends_full_file(app, httpreq, tmpdir):
123
+ filename = path.join(tmpdir, 'range.txt')
124
+ with open(filename, 'wb') as f:
125
+ f.write(b'0123456789')
126
+ app.staticfile(r'/range\.txt', filename)
127
+
128
+ with httpreq('/range.txt', headers={
129
+ 'Range': 'bytes=2-5',
130
+ 'If-Range': '"unknown-validator"',
131
+ }):
132
+ assert status == 200
133
+ assert response == '0123456789'
134
+
135
+
20
136
  def test_staticdirectory(app, httpreq, mktmptree):
21
137
  temproot = mktmptree({
22
138
  'bar': {
@@ -0,0 +1,145 @@
1
+ import fnmatch
2
+ from glob import glob
3
+ from os import path
4
+ from queue import Empty, Queue
5
+ import time
6
+
7
+ from watchdog.events import FileSystemEventHandler
8
+ from watchdog.observers import Observer
9
+
10
+ from .logging import logger
11
+
12
+
13
+ DEFAULT_EXCLUDEDIRECTORIES = [
14
+ '**/__pycache__',
15
+ '**/.*',
16
+ '**/~*',
17
+ ]
18
+ DEFAULT_EXCLUDEFILES = [
19
+ '**/.*',
20
+ '**/*~',
21
+ '**/4913',
22
+ ]
23
+
24
+
25
+ class _EventHandler(FileSystemEventHandler):
26
+ def __init__(self, watcher):
27
+ self.watcher = watcher
28
+
29
+ def on_any_event(self, event):
30
+ if event.event_type not in ('created', 'deleted', 'modified', 'moved'):
31
+ return
32
+ # Watchdog reports a directory modification when an entry inside it
33
+ # changes. Inotify only reported the entry itself for the masks we
34
+ # used, so directory metadata events must not become change results.
35
+ if event.is_directory and event.event_type == 'modified':
36
+ return
37
+ self.watcher._events.put(event)
38
+
39
+
40
+ class FSWatcher:
41
+ def __init__(self, directories=None, files=None, excludedirectories=None,
42
+ excludefiles=None):
43
+ assert directories or files
44
+ self.directories = directories or []
45
+ self.files = files or []
46
+ self._observer = Observer()
47
+ self._events = Queue()
48
+ self._handler = _EventHandler(self)
49
+ self._watched_directories = set()
50
+ self._watchfiles = set()
51
+
52
+ self.excludedirectories = list(DEFAULT_EXCLUDEDIRECTORIES)
53
+ if excludedirectories:
54
+ self.excludedirectories.extend(excludedirectories)
55
+
56
+ self.excludefiles = list(DEFAULT_EXCLUDEFILES)
57
+ if excludefiles:
58
+ self.excludefiles.extend(excludefiles)
59
+
60
+ def _excludedirectory(self, node):
61
+ return any(fnmatch.fnmatchcase(node, pattern)
62
+ for pattern in self.excludedirectories)
63
+
64
+ def _excludefile(self, node):
65
+ return any(fnmatch.fnmatchcase(node, pattern)
66
+ for pattern in self.excludefiles)
67
+
68
+ def _refine_watches(self):
69
+ self._watched_directories = set()
70
+ self._watchfiles = set()
71
+
72
+ for pattern in self.directories:
73
+ for node in glob(pattern, recursive=True):
74
+ if path.isdir(node) and not self._excludedirectory(node):
75
+ self._watched_directories.add(path.abspath(node))
76
+
77
+ for pattern in self.files:
78
+ for node in glob(pattern, recursive=True):
79
+ if path.isfile(node) and not self._excludefile(node):
80
+ self._watchfiles.add(path.abspath(node))
81
+
82
+ def start(self):
83
+ self._refine_watches()
84
+ directories = set(self._watched_directories)
85
+ directories.update(path.dirname(node) for node in self._watchfiles)
86
+ for directory in directories:
87
+ logger.info(f'Adding watch for: {directory}')
88
+ self._observer.schedule(self._handler, directory, recursive=False)
89
+ self._observer.start()
90
+
91
+ def stop(self):
92
+ if self._observer.is_alive():
93
+ self._observer.stop()
94
+ self._observer.join()
95
+ self._clear_events()
96
+
97
+ def close(self):
98
+ self.stop()
99
+
100
+ def _clear_events(self):
101
+ while True:
102
+ try:
103
+ self._events.get_nowait()
104
+ except Empty:
105
+ break
106
+
107
+ def _event_paths(self, event):
108
+ paths = [event.src_path]
109
+ if event.event_type == 'moved':
110
+ paths.append(event.dest_path)
111
+
112
+ matching = set()
113
+ for node in paths:
114
+ node = path.abspath(node)
115
+ if self._excludefile(node):
116
+ continue
117
+ if node in self._watchfiles:
118
+ matching.add(node)
119
+ if (node in self._watched_directories or path.dirname(node) in
120
+ self._watched_directories):
121
+ matching.add(node)
122
+ return matching
123
+
124
+ def wait(self, timeout=0):
125
+ changes = set()
126
+ deadline = time.monotonic() + max(timeout, 0) / 1000
127
+ # Watchdog delivers notifications asynchronously. A short initial wait
128
+ # lets zero-debounce callers observe events caused just before wait().
129
+ initial_wait = 0.25 if timeout <= 0 else max(timeout, 0) / 1000
130
+ while True:
131
+ if timeout <= 0:
132
+ remaining = initial_wait
133
+ else:
134
+ remaining = max(0, deadline - time.monotonic())
135
+ try:
136
+ event = self._events.get(timeout=remaining)
137
+ except Empty:
138
+ break
139
+ changes.update(self._event_paths(event))
140
+ if changes and timeout > 0 and time.monotonic() >= deadline:
141
+ break
142
+ if changes and timeout <= 0:
143
+ # Drain notifications already queued in this delivery batch.
144
+ continue
145
+ return changes
@@ -8,21 +8,94 @@ from . import statuses
8
8
  CHUNKSIZE = 1024 * 10
9
9
 
10
10
 
11
+ def _byte_range(request, length):
12
+ """Return the selected byte interval, or None when Range is ignored."""
13
+ if request.verb != 'get' or 'HTTP_IF_RANGE' in request.environ:
14
+ return None
15
+
16
+ value = request.environ.get('HTTP_RANGE')
17
+ if not value or '=' not in value:
18
+ return None
19
+
20
+ unit, spec = value.split('=', 1)
21
+ if unit.strip().lower() != 'bytes' or ',' in spec:
22
+ # Multiple ranges are optional; ignore them and send the full entity.
23
+ return None
24
+
25
+ spec = spec.strip()
26
+ if '-' not in spec:
27
+ return None
28
+ first, last = (part.strip() for part in spec.split('-', 1))
29
+ if not first and not last:
30
+ return None
31
+
32
+ try:
33
+ if not first:
34
+ suffix_length = int(last)
35
+ if suffix_length < 0:
36
+ return None
37
+ if suffix_length == 0 or length == 0:
38
+ return False
39
+ return max(0, length - suffix_length), length - 1
40
+
41
+ start = int(first)
42
+ if last:
43
+ end = int(last)
44
+ if end < start:
45
+ return None
46
+ else:
47
+ end = length - 1
48
+ except ValueError:
49
+ return None
50
+
51
+ if start >= length:
52
+ return False
53
+ return start, min(end, length - 1)
54
+
55
+
56
+ def _stream_file(request, filename, length, contenttype):
57
+ response = request.response
58
+ response.contenttype = contenttype
59
+ response.headers['accept-ranges'] = 'bytes'
60
+
61
+ byte_range = _byte_range(request, length)
62
+ if byte_range is False:
63
+ response.status = '416 Range Not Satisfiable'
64
+ response.headers['content-range'] = f'bytes */{length}'
65
+ response.length = 0
66
+ yield b''
67
+ return
68
+
69
+ if byte_range is None:
70
+ response.length = length
71
+ start, end = 0, length - 1
72
+ else:
73
+ start, end = byte_range
74
+ response.status = '206 Partial Content'
75
+ response.headers['content-range'] = f'bytes {start}-{end}/{length}'
76
+ response.length = end - start + 1
77
+
78
+ if not response.length:
79
+ yield b''
80
+ return
81
+
82
+ with open(filename, 'rb') as f:
83
+ f.seek(start)
84
+ remaining = response.length
85
+ while remaining:
86
+ chunk = f.read(min(CHUNKSIZE, remaining))
87
+ if not chunk:
88
+ break
89
+ remaining -= len(chunk)
90
+ yield chunk
91
+
92
+
11
93
  def file(filename):
12
94
  length = path.getsize(filename)
13
95
  type_ = guess_type(path.split(filename)[1])[0]
14
96
 
15
97
  def get(request):
16
- response = request.response
17
- response.length = length
18
- response.contenttype = type_
19
- with open(filename, 'rb') as f:
20
- while True:
21
- chunk = f.read(CHUNKSIZE)
22
- if not chunk:
23
- break
24
-
25
- yield chunk
98
+ yield from _stream_file(request, filename, length, type_)
26
99
 
27
100
  return get
28
101
 
@@ -145,14 +218,11 @@ def directory(rootpath, default=False, autoindex=True, fallback=False):
145
218
  if not path.exists(target):
146
219
  raise statuses.notfound()
147
220
 
148
- response.length = path.getsize(target)
149
- response.contenttype = guess_type(path.split(target)[1])[0]
150
- with open(target, 'rb') as f:
151
- while True:
152
- chunk = f.read(CHUNKSIZE)
153
- if not chunk:
154
- break
155
-
156
- yield chunk
221
+ yield from _stream_file(
222
+ request,
223
+ target,
224
+ path.getsize(target),
225
+ guess_type(path.split(target)[1])[0],
226
+ )
157
227
 
158
228
  return get
@@ -0,0 +1 @@
1
+ __version__ = '9.6.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yhttp
3
- Version: 9.4.2
3
+ Version: 9.6.0
4
4
  Summary: A very micro http framework.
5
5
  Home-page: http://github.com/yhttp/yhttp
6
6
  Author: Vahid Mardani
@@ -29,7 +29,7 @@ License-File: LICENSE
29
29
  Requires-Dist: snam>=1.0.2
30
30
  Requires-Dist: easycli<2,>=1.10.1
31
31
  Requires-Dist: ujson
32
- Requires-Dist: inotify-simple
32
+ Requires-Dist: watchdog
33
33
  Dynamic: author
34
34
  Dynamic: author-email
35
35
  Dynamic: classifier
@@ -1,4 +1,4 @@
1
1
  snam>=1.0.2
2
2
  easycli<2,>=1.10.1
3
3
  ujson
4
- inotify-simple
4
+ watchdog
@@ -1,137 +0,0 @@
1
- import time
2
- from os import path
3
- from glob import glob
4
- import fnmatch
5
-
6
- from inotify_simple import INotify, flags
7
- from .logging import logger
8
-
9
-
10
- DEFAULT_EXCLUDEDIRECTORIES = [
11
- '**/__pycache__',
12
- '**/.*',
13
- '**/~*',
14
- ]
15
- DEFAULT_EXCLUDEFILES = [
16
- '**/.*',
17
- '**/*~',
18
- '**/4913',
19
- ]
20
-
21
-
22
- class FSWatcher(INotify):
23
- def __init__(self, directories=None, files=None, excludedirectories=None,
24
- excludefiles=None):
25
- assert directories or files
26
- super().__init__(closefd=False)
27
- self._directorymask = flags.CREATE \
28
- | flags.DELETE \
29
- | flags.DELETE_SELF \
30
- | flags.MOVE_SELF \
31
- | flags.MOVED_FROM \
32
- | flags.MOVED_TO \
33
- | flags.MODIFY
34
-
35
- self._filemask = flags.MODIFY | flags.DELETE_SELF | flags.MOVE_SELF
36
- self.directories = directories or []
37
- self.files = files or []
38
- self._wds = {}
39
-
40
- self.excludedirectories = []
41
- self.excludedirectories.extend(DEFAULT_EXCLUDEDIRECTORIES)
42
- if excludedirectories:
43
- self.excludedirectories.extend(excludedirectories)
44
-
45
- self.excludefiles = []
46
- self.excludefiles.extend(DEFAULT_EXCLUDEFILES)
47
- if excludefiles:
48
- self.excludefiles.extend(excludefiles)
49
-
50
- def _excludedirectory(self, node):
51
- for pattern in self.excludedirectories:
52
- if fnmatch.fnmatchcase(node, pattern):
53
- return True
54
-
55
- return False
56
-
57
- def _excludefile(self, node):
58
- for pattern in self.excludefiles:
59
- if fnmatch.fnmatchcase(node, pattern):
60
- return True
61
-
62
- return False
63
-
64
- def _refine_watches(self):
65
- self._watchdirectories = set()
66
- self._watchfiles = set()
67
-
68
- for pattern in self.directories:
69
- matches = glob(pattern, recursive=True)
70
- for node in matches:
71
- if path.isdir(node) and not self._excludedirectory(node):
72
- self._watchdirectories.add(node)
73
-
74
- for pattern in self.files:
75
- matches = glob(pattern, recursive=True)
76
- for node in matches:
77
- if path.isfile(node) and not self._excludefile(node):
78
- self._watchfiles.add(node)
79
-
80
- def watch(self, node, mask):
81
- logger.info(f'Adding watch for: {node}')
82
- wd = self.add_watch(node, mask)
83
- self._wds[wd] = node
84
-
85
- def start(self):
86
- self._refine_watches()
87
- for node in self._watchdirectories:
88
- self.watch(node, self._directorymask)
89
-
90
- for node in self._watchfiles:
91
- self.watch(node, self._filemask)
92
-
93
- def stop(self):
94
- self._readall()
95
- for wd in self._wds:
96
- self.rm_watch(wd)
97
-
98
- self._wds = {}
99
-
100
- def close(self):
101
- self._poller.unregister(self.fileno())
102
- super().close()
103
-
104
- def wait(self, timeout=0):
105
- changes = set()
106
- starttime = None
107
- while True:
108
- for e in self.read(timeout):
109
- # cannot simulate this situation, but I think it's enough to
110
- # raise error
111
- if e.wd == -1: # pragma: no cover
112
- raise OSError('INotify event queue overflow')
113
-
114
- # uncomment to debug masks(flags)
115
- # fl = [f.name for f in flags.from_mask(e.mask)]
116
- if e.mask & flags.IGNORED:
117
- continue
118
-
119
- if not (e.mask & flags.ISDIR) and e.name:
120
- fn = path.join(self._wds[e.wd], e.name)
121
- if e.name and self._excludefile(fn):
122
- continue
123
- else:
124
- fn = self._wds[e.wd]
125
-
126
- changes.add(fn)
127
-
128
- if changes:
129
- if starttime is None:
130
- starttime = time.perf_counter()
131
- continue
132
- timeout -= (time.perf_counter() - starttime) * 1000
133
-
134
- if timeout <= 0:
135
- break
136
-
137
- return changes
@@ -1 +0,0 @@
1
- __version__ = '9.4.2'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes