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.
- {yhttp-9.4.2/yhttp.egg-info → yhttp-9.6.0}/PKG-INFO +2 -2
- {yhttp-9.4.2 → yhttp-9.6.0}/setup.py +1 -1
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_fswatcher.py +31 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_static.py +116 -0
- yhttp-9.6.0/yhttp/core/fswatcher.py +145 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/static.py +89 -19
- yhttp-9.6.0/yhttp/core/version.py +1 -0
- {yhttp-9.4.2 → yhttp-9.6.0/yhttp.egg-info}/PKG-INFO +2 -2
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/requires.txt +1 -1
- yhttp-9.4.2/yhttp/core/fswatcher.py +0 -137
- yhttp-9.4.2/yhttp/core/version.py +0 -1
- {yhttp-9.4.2 → yhttp-9.6.0}/LICENSE +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/README.md +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/setup.cfg +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_application.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_applicationcli.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_serve.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_version.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_builtincli_yhttp_version.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_cookie.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_cookieset.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_extension.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_json.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_multipart.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_form_urlencoded.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_body.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_contenttypes.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_guard_query.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_headerset.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_jsonencoding.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_locale.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_logging.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_middleware.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_multidict.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_multipart.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_querystring.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_request.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_responseheaders.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_rewrite.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_rewritecli.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_routing.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_statuses.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/tests/test_statushandler.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/__init__.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/application.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/cli.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/contenttypes.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/cookieset.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/guard.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/headerset.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/logging.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/main.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/multidict.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/multipart.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/request.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/response.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/rewrite.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp/core/statuses.py +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/SOURCES.txt +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/dependency_links.txt +0 -0
- {yhttp-9.4.2 → yhttp-9.6.0}/yhttp.egg-info/entry_points.txt +0 -0
- {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.
|
|
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:
|
|
32
|
+
Requires-Dist: watchdog
|
|
33
33
|
Dynamic: author
|
|
34
34
|
Dynamic: author-email
|
|
35
35
|
Dynamic: classifier
|
|
@@ -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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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:
|
|
32
|
+
Requires-Dist: watchdog
|
|
33
33
|
Dynamic: author
|
|
34
34
|
Dynamic: author-email
|
|
35
35
|
Dynamic: classifier
|
|
@@ -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
|
|
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
|