ezKit 1.11.13__py3-none-any.whl → 1.11.15__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ezKit/bottle.py +287 -178
- ezKit/utils.py +21 -13
- {ezKit-1.11.13.dist-info → ezKit-1.11.15.dist-info}/METADATA +7 -2
- {ezKit-1.11.13.dist-info → ezKit-1.11.15.dist-info}/RECORD +7 -7
- {ezKit-1.11.13.dist-info → ezKit-1.11.15.dist-info}/WHEEL +1 -1
- {ezKit-1.11.13.dist-info → ezKit-1.11.15.dist-info}/LICENSE +0 -0
- {ezKit-1.11.13.dist-info → ezKit-1.11.15.dist-info}/top_level.txt +0 -0
ezKit/bottle.py
CHANGED
@@ -14,6 +14,7 @@ License: MIT (see LICENSE for details)
|
|
14
14
|
"""
|
15
15
|
|
16
16
|
from __future__ import print_function
|
17
|
+
|
17
18
|
import sys
|
18
19
|
|
19
20
|
__author__ = 'Marcel Hellkamp'
|
@@ -62,48 +63,66 @@ def _cli_patch(cli_args): # pragma: no coverage
|
|
62
63
|
eventlet.monkey_patch()
|
63
64
|
|
64
65
|
|
65
|
-
if __name__ ==
|
66
|
+
if __name__ == "__main__":
|
66
67
|
_cli_patch(sys.argv)
|
67
68
|
|
68
69
|
###############################################################################
|
69
70
|
# Imports and Python 2/3 unification ##########################################
|
70
71
|
###############################################################################
|
71
72
|
|
72
|
-
import base64
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
import base64
|
74
|
+
import calendar
|
75
|
+
import email.utils
|
76
|
+
import functools
|
77
|
+
import hashlib
|
78
|
+
import hmac
|
79
|
+
import itertools
|
80
|
+
import mimetypes
|
81
|
+
import os
|
82
|
+
import re
|
83
|
+
import tempfile
|
84
|
+
import threading
|
85
|
+
import time
|
86
|
+
import warnings
|
87
|
+
import weakref
|
88
|
+
from datetime import date as datedate
|
89
|
+
from datetime import datetime, timedelta
|
77
90
|
from tempfile import NamedTemporaryFile
|
78
91
|
from traceback import format_exc, print_exc
|
92
|
+
from types import FunctionType
|
79
93
|
from unicodedata import normalize
|
80
94
|
|
81
95
|
try:
|
82
|
-
from ujson import dumps as json_dumps
|
96
|
+
from ujson import dumps as json_dumps
|
97
|
+
from ujson import loads as json_lds
|
83
98
|
except ImportError:
|
84
|
-
from json import dumps as json_dumps
|
99
|
+
from json import dumps as json_dumps
|
100
|
+
from json import loads as json_lds
|
85
101
|
|
86
102
|
py = sys.version_info
|
87
103
|
py3k = py.major > 2
|
88
104
|
|
89
105
|
# Lots of stdlib and builtin differences.
|
90
106
|
if py3k:
|
91
|
-
import http.client as httplib
|
92
107
|
import _thread as thread
|
93
|
-
|
94
|
-
from urllib.parse import
|
108
|
+
import http.client as httplib
|
109
|
+
from urllib.parse import SplitResult as UrlSplitResult
|
110
|
+
from urllib.parse import quote as urlquote
|
111
|
+
from urllib.parse import unquote as urlunquote
|
112
|
+
from urllib.parse import urlencode, urljoin
|
95
113
|
urlunquote = functools.partial(urlunquote, encoding='latin1')
|
96
|
-
from http.cookies import SimpleCookie, Morsel, CookieError
|
97
|
-
from collections.abc import MutableMapping as DictMixin
|
98
|
-
from types import ModuleType as new_module
|
99
|
-
import pickle
|
100
|
-
from io import BytesIO
|
101
114
|
import configparser
|
115
|
+
import pickle
|
116
|
+
from collections.abc import MutableMapping as DictMixin
|
102
117
|
from datetime import timezone
|
118
|
+
from http.cookies import CookieError, Morsel, SimpleCookie
|
119
|
+
from io import BytesIO
|
120
|
+
from types import ModuleType as new_module
|
103
121
|
UTC = timezone.utc
|
104
122
|
# getfullargspec was deprecated in 3.5 and un-deprecated in 3.6
|
105
123
|
# getargspec was deprecated in 3.0 and removed in 3.11
|
106
124
|
from inspect import getfullargspec
|
125
|
+
|
107
126
|
def getargspec(func):
|
108
127
|
spec = getfullargspec(func)
|
109
128
|
kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs)
|
@@ -111,27 +130,31 @@ if py3k:
|
|
111
130
|
|
112
131
|
basestring = str
|
113
132
|
unicode = str
|
114
|
-
json_loads
|
115
|
-
callable
|
133
|
+
def json_loads(s): return json_lds(touni(s))
|
134
|
+
def callable(x): return hasattr(x, '__call__')
|
116
135
|
imap = map
|
117
136
|
|
118
137
|
def _raise(*a):
|
119
138
|
raise a[0](a[1]).with_traceback(a[2])
|
120
139
|
else: # 2.x
|
121
140
|
warnings.warn("Python 2 support will be dropped in Bottle 0.14", DeprecationWarning)
|
122
|
-
import
|
123
|
-
import
|
124
|
-
from
|
125
|
-
from
|
126
|
-
from Cookie import SimpleCookie, Morsel, CookieError
|
141
|
+
from collections import MutableMapping as DictMixin
|
142
|
+
from datetime import tzinfo
|
143
|
+
from imp import new_module
|
144
|
+
from inspect import getargspec
|
127
145
|
from itertools import imap
|
146
|
+
from urllib import quote as urlquote
|
147
|
+
from urllib import unquote as urlunquote
|
148
|
+
from urllib import urlencode
|
149
|
+
|
150
|
+
import ConfigParser as configparser
|
128
151
|
import cPickle as pickle
|
129
|
-
|
152
|
+
import httplib
|
153
|
+
import thread
|
154
|
+
from Cookie import CookieError, Morsel, SimpleCookie
|
130
155
|
from StringIO import StringIO as BytesIO
|
131
|
-
import
|
132
|
-
from
|
133
|
-
from inspect import getargspec
|
134
|
-
from datetime import tzinfo
|
156
|
+
from urlparse import SplitResult as UrlSplitResult
|
157
|
+
from urlparse import urljoin
|
135
158
|
|
136
159
|
class _UTC(tzinfo):
|
137
160
|
def utcoffset(self, dt): return timedelta(0)
|
@@ -164,7 +187,7 @@ def _stderr(*args):
|
|
164
187
|
try:
|
165
188
|
print(*args, file=sys.stderr)
|
166
189
|
except (IOError, AttributeError):
|
167
|
-
pass
|
190
|
+
pass # Some environments do not allow printing (mod_wsgi)
|
168
191
|
|
169
192
|
|
170
193
|
# A bug in functools causes it to break if the wrapper is an instance method
|
@@ -209,17 +232,21 @@ class DictProperty(object):
|
|
209
232
|
return self
|
210
233
|
|
211
234
|
def __get__(self, obj, cls):
|
212
|
-
if obj is None:
|
235
|
+
if obj is None:
|
236
|
+
return self
|
213
237
|
key, storage = self.key, getattr(obj, self.attr)
|
214
|
-
if key not in storage:
|
238
|
+
if key not in storage:
|
239
|
+
storage[key] = self.getter(obj)
|
215
240
|
return storage[key]
|
216
241
|
|
217
242
|
def __set__(self, obj, value):
|
218
|
-
if self.read_only:
|
243
|
+
if self.read_only:
|
244
|
+
raise AttributeError("Read-Only property.")
|
219
245
|
getattr(obj, self.attr)[self.key] = value
|
220
246
|
|
221
247
|
def __delete__(self, obj):
|
222
|
-
if self.read_only:
|
248
|
+
if self.read_only:
|
249
|
+
raise AttributeError("Read-Only property.")
|
223
250
|
del getattr(obj, self.attr)[self.key]
|
224
251
|
|
225
252
|
|
@@ -233,7 +260,8 @@ class cached_property(object):
|
|
233
260
|
self.func = func
|
234
261
|
|
235
262
|
def __get__(self, obj, cls):
|
236
|
-
if obj is None:
|
263
|
+
if obj is None:
|
264
|
+
return self
|
237
265
|
value = obj.__dict__[self.func.__name__] = self.func(obj)
|
238
266
|
return value
|
239
267
|
|
@@ -339,9 +367,9 @@ class Router(object):
|
|
339
367
|
self.filters[name] = func
|
340
368
|
|
341
369
|
rule_syntax = re.compile('(\\\\*)'
|
342
|
-
|
343
|
-
|
344
|
-
|
370
|
+
'(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'
|
371
|
+
'|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'
|
372
|
+
'(?::((?:\\\\.|[^\\\\>])+)?)?)?>))')
|
345
373
|
|
346
374
|
def _itertokens(self, rule):
|
347
375
|
offset, prefix = 0, ''
|
@@ -376,7 +404,8 @@ class Router(object):
|
|
376
404
|
for key, mode, conf in self._itertokens(rule):
|
377
405
|
if mode:
|
378
406
|
is_static = False
|
379
|
-
if mode == 'default':
|
407
|
+
if mode == 'default':
|
408
|
+
mode = self.default_filter
|
380
409
|
mask, in_filter, out_filter = self.filters[mode](conf)
|
381
410
|
if not key:
|
382
411
|
pattern += '(?:%s)' % mask
|
@@ -385,14 +414,16 @@ class Router(object):
|
|
385
414
|
else:
|
386
415
|
pattern += '(?P<%s>%s)' % (key, mask)
|
387
416
|
keys.append(key)
|
388
|
-
if in_filter:
|
417
|
+
if in_filter:
|
418
|
+
filters.append((key, in_filter))
|
389
419
|
builder.append((key, out_filter or str))
|
390
420
|
elif key:
|
391
421
|
pattern += re.escape(key)
|
392
422
|
builder.append((None, key))
|
393
423
|
|
394
424
|
self.builder[rule] = builder
|
395
|
-
if name:
|
425
|
+
if name:
|
426
|
+
self.builder[name] = builder
|
396
427
|
|
397
428
|
if is_static and not self.strict_order:
|
398
429
|
self.static.setdefault(method, {})
|
@@ -548,11 +579,15 @@ class Route(object):
|
|
548
579
|
""" Yield all Plugins affecting this route. """
|
549
580
|
unique = set()
|
550
581
|
for p in reversed(self.app.plugins + self.plugins):
|
551
|
-
if True in self.skiplist:
|
582
|
+
if True in self.skiplist:
|
583
|
+
break
|
552
584
|
name = getattr(p, 'name', False)
|
553
|
-
if name and (name in self.skiplist or name in unique):
|
554
|
-
|
555
|
-
if
|
585
|
+
if name and (name in self.skiplist or name in unique):
|
586
|
+
continue
|
587
|
+
if p in self.skiplist or type(p) in self.skiplist:
|
588
|
+
continue
|
589
|
+
if name:
|
590
|
+
unique.add(name)
|
556
591
|
yield p
|
557
592
|
|
558
593
|
def _make_callback(self):
|
@@ -815,7 +850,8 @@ class Bottle(object):
|
|
815
850
|
applied to all routes of this application. A plugin may be a simple
|
816
851
|
decorator or an object that implements the :class:`Plugin` API.
|
817
852
|
"""
|
818
|
-
if hasattr(plugin, 'setup'):
|
853
|
+
if hasattr(plugin, 'setup'):
|
854
|
+
plugin.setup(self)
|
819
855
|
if not callable(plugin) and not hasattr(plugin, 'apply'):
|
820
856
|
raise TypeError("Plugins must be callable or implement .apply()")
|
821
857
|
self.plugins.append(plugin)
|
@@ -830,20 +866,25 @@ class Bottle(object):
|
|
830
866
|
removed, remove = [], plugin
|
831
867
|
for i, plugin in list(enumerate(self.plugins))[::-1]:
|
832
868
|
if remove is True or remove is plugin or remove is type(plugin) \
|
833
|
-
|
869
|
+
or getattr(plugin, 'name', True) == remove:
|
834
870
|
removed.append(plugin)
|
835
871
|
del self.plugins[i]
|
836
|
-
if hasattr(plugin, 'close'):
|
837
|
-
|
872
|
+
if hasattr(plugin, 'close'):
|
873
|
+
plugin.close()
|
874
|
+
if removed:
|
875
|
+
self.reset()
|
838
876
|
return removed
|
839
877
|
|
840
878
|
def reset(self, route=None):
|
841
879
|
""" Reset all routes (force plugins to be re-applied) and clear all
|
842
880
|
caches. If an ID or route object is given, only that specific route
|
843
881
|
is affected. """
|
844
|
-
if route is None:
|
845
|
-
|
846
|
-
|
882
|
+
if route is None:
|
883
|
+
routes = self.routes
|
884
|
+
elif isinstance(route, Route):
|
885
|
+
routes = [route]
|
886
|
+
else:
|
887
|
+
routes = [self.routes[route]]
|
847
888
|
for route in routes:
|
848
889
|
route.reset()
|
849
890
|
if DEBUG:
|
@@ -854,7 +895,8 @@ class Bottle(object):
|
|
854
895
|
def close(self):
|
855
896
|
""" Close the application and all installed plugins. """
|
856
897
|
for plugin in self.plugins:
|
857
|
-
if hasattr(plugin, 'close'):
|
898
|
+
if hasattr(plugin, 'close'):
|
899
|
+
plugin.close()
|
858
900
|
|
859
901
|
def run(self, **kwargs):
|
860
902
|
""" Calls :func:`run` with the same parameters. """
|
@@ -877,7 +919,8 @@ class Bottle(object):
|
|
877
919
|
attribute."""
|
878
920
|
self.routes.append(route)
|
879
921
|
self.router.add(route.rule, route.method, route, name=route.name)
|
880
|
-
if DEBUG:
|
922
|
+
if DEBUG:
|
923
|
+
route.prepare()
|
881
924
|
|
882
925
|
def route(self,
|
883
926
|
path=None,
|
@@ -911,12 +954,14 @@ class Bottle(object):
|
|
911
954
|
Any additional keyword arguments are stored as route-specific
|
912
955
|
configuration and passed to plugins (see :meth:`Plugin.apply`).
|
913
956
|
"""
|
914
|
-
if callable(path):
|
957
|
+
if callable(path):
|
958
|
+
path, callback = None, path
|
915
959
|
plugins = makelist(apply)
|
916
960
|
skiplist = makelist(skip)
|
917
961
|
|
918
962
|
def decorator(callback):
|
919
|
-
if isinstance(callback, basestring):
|
963
|
+
if isinstance(callback, basestring):
|
964
|
+
callback = load(callback)
|
920
965
|
for rule in makelist(path) or yieldroutes(callback):
|
921
966
|
for verb in makelist(method):
|
922
967
|
verb = verb.upper()
|
@@ -965,7 +1010,8 @@ class Bottle(object):
|
|
965
1010
|
"""
|
966
1011
|
|
967
1012
|
def decorator(callback):
|
968
|
-
if isinstance(callback, basestring):
|
1013
|
+
if isinstance(callback, basestring):
|
1014
|
+
callback = load(callback)
|
969
1015
|
self.error_handler[int(code)] = callback
|
970
1016
|
return callback
|
971
1017
|
|
@@ -984,7 +1030,7 @@ class Bottle(object):
|
|
984
1030
|
response.bind()
|
985
1031
|
|
986
1032
|
try:
|
987
|
-
while True:
|
1033
|
+
while True: # Remove in 0.14 together with RouteReset
|
988
1034
|
out = None
|
989
1035
|
try:
|
990
1036
|
self.trigger_hook('before_request')
|
@@ -1014,7 +1060,8 @@ class Bottle(object):
|
|
1014
1060
|
except (KeyboardInterrupt, SystemExit, MemoryError):
|
1015
1061
|
raise
|
1016
1062
|
except Exception as E:
|
1017
|
-
if not self.catchall:
|
1063
|
+
if not self.catchall:
|
1064
|
+
raise
|
1018
1065
|
stacktrace = format_exc()
|
1019
1066
|
environ['wsgi.errors'].write(stacktrace)
|
1020
1067
|
environ['wsgi.errors'].flush()
|
@@ -1038,7 +1085,7 @@ class Bottle(object):
|
|
1038
1085
|
return []
|
1039
1086
|
# Join lists of byte or unicode strings. Mixed lists are NOT supported
|
1040
1087
|
if isinstance(out, (tuple, list))\
|
1041
|
-
|
1088
|
+
and isinstance(out[0], (bytes, unicode)):
|
1042
1089
|
out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
|
1043
1090
|
# Encode unicode strings
|
1044
1091
|
if isinstance(out, unicode):
|
@@ -1079,7 +1126,8 @@ class Bottle(object):
|
|
1079
1126
|
except (KeyboardInterrupt, SystemExit, MemoryError):
|
1080
1127
|
raise
|
1081
1128
|
except Exception as error:
|
1082
|
-
if not self.catchall:
|
1129
|
+
if not self.catchall:
|
1130
|
+
raise
|
1083
1131
|
first = HTTPError(500, 'Unhandled exception', error, format_exc())
|
1084
1132
|
|
1085
1133
|
# These are the inner types allowed in iterator or generator objects.
|
@@ -1088,7 +1136,7 @@ class Bottle(object):
|
|
1088
1136
|
elif isinstance(first, bytes):
|
1089
1137
|
new_iter = itertools.chain([first], iout)
|
1090
1138
|
elif isinstance(first, unicode):
|
1091
|
-
encoder
|
1139
|
+
def encoder(x): return x.encode(response.charset)
|
1092
1140
|
new_iter = imap(encoder, itertools.chain([first], iout))
|
1093
1141
|
else:
|
1094
1142
|
msg = 'Unsupported response type: %s' % type(first)
|
@@ -1103,8 +1151,9 @@ class Bottle(object):
|
|
1103
1151
|
out = self._cast(self._handle(environ))
|
1104
1152
|
# rfc2616 section 4.3
|
1105
1153
|
if response._status_code in (100, 101, 204, 304)\
|
1106
|
-
|
1107
|
-
if hasattr(out, 'close'):
|
1154
|
+
or environ['REQUEST_METHOD'] == 'HEAD':
|
1155
|
+
if hasattr(out, 'close'):
|
1156
|
+
out.close()
|
1108
1157
|
out = []
|
1109
1158
|
exc_info = environ.get('bottle.exc_info')
|
1110
1159
|
if exc_info is not None:
|
@@ -1114,7 +1163,8 @@ class Bottle(object):
|
|
1114
1163
|
except (KeyboardInterrupt, SystemExit, MemoryError):
|
1115
1164
|
raise
|
1116
1165
|
except Exception as E:
|
1117
|
-
if not self.catchall:
|
1166
|
+
if not self.catchall:
|
1167
|
+
raise
|
1118
1168
|
err = '<h1>Critical error while processing request: %s</h1>' \
|
1119
1169
|
% html_escape(environ.get('PATH_INFO', '/'))
|
1120
1170
|
if DEBUG:
|
@@ -1303,7 +1353,8 @@ class BaseRequest(object):
|
|
1303
1353
|
maxread = max(0, self.content_length)
|
1304
1354
|
while maxread:
|
1305
1355
|
part = read(min(maxread, bufsize))
|
1306
|
-
if not part:
|
1356
|
+
if not part:
|
1357
|
+
break
|
1307
1358
|
yield part
|
1308
1359
|
maxread -= len(part)
|
1309
1360
|
|
@@ -1316,20 +1367,24 @@ class BaseRequest(object):
|
|
1316
1367
|
while header[-2:] != rn:
|
1317
1368
|
c = read(1)
|
1318
1369
|
header += c
|
1319
|
-
if not c:
|
1320
|
-
|
1370
|
+
if not c:
|
1371
|
+
raise err
|
1372
|
+
if len(header) > bufsize:
|
1373
|
+
raise err
|
1321
1374
|
size, _, _ = header.partition(sem)
|
1322
1375
|
try:
|
1323
1376
|
maxread = int(tonat(size.strip()), 16)
|
1324
1377
|
except ValueError:
|
1325
1378
|
raise err
|
1326
|
-
if maxread == 0:
|
1379
|
+
if maxread == 0:
|
1380
|
+
break
|
1327
1381
|
buff = bs
|
1328
1382
|
while maxread > 0:
|
1329
1383
|
if not buff:
|
1330
1384
|
buff = read(min(maxread, bufsize))
|
1331
1385
|
part, buff = buff[:maxread], buff[maxread:]
|
1332
|
-
if not part:
|
1386
|
+
if not part:
|
1387
|
+
raise err
|
1333
1388
|
yield part
|
1334
1389
|
maxread -= len(part)
|
1335
1390
|
if read(2) != rn:
|
@@ -1408,15 +1463,15 @@ class BaseRequest(object):
|
|
1408
1463
|
if not boundary:
|
1409
1464
|
raise MultipartError("Invalid content type header, missing boundary")
|
1410
1465
|
parser = _MultipartParser(self.body, boundary, self.content_length,
|
1411
|
-
|
1412
|
-
|
1466
|
+
mem_limit=self.MEMFILE_MAX, memfile_limit=self.MEMFILE_MAX,
|
1467
|
+
charset=charset)
|
1413
1468
|
|
1414
1469
|
for part in parser.parse():
|
1415
1470
|
if not part.filename and part.is_buffered():
|
1416
1471
|
post[part.name] = tonat(part.value, 'utf8')
|
1417
1472
|
else:
|
1418
1473
|
post[part.name] = FileUpload(part.file, part.name,
|
1419
|
-
|
1474
|
+
part.filename, part.headerlist)
|
1420
1475
|
|
1421
1476
|
return post
|
1422
1477
|
|
@@ -1436,7 +1491,7 @@ class BaseRequest(object):
|
|
1436
1491
|
server. """
|
1437
1492
|
env = self.environ
|
1438
1493
|
http = env.get('HTTP_X_FORWARDED_PROTO') \
|
1439
|
-
|
1494
|
+
or env.get('wsgi.url_scheme', 'http')
|
1440
1495
|
host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
|
1441
1496
|
if not host:
|
1442
1497
|
# HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
|
@@ -1511,9 +1566,11 @@ class BaseRequest(object):
|
|
1511
1566
|
the user field is looked up from the ``REMOTE_USER`` environ
|
1512
1567
|
variable. On any errors, None is returned. """
|
1513
1568
|
basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', ''))
|
1514
|
-
if basic:
|
1569
|
+
if basic:
|
1570
|
+
return basic
|
1515
1571
|
ruser = self.environ.get('REMOTE_USER')
|
1516
|
-
if ruser:
|
1572
|
+
if ruser:
|
1573
|
+
return (ruser, None)
|
1517
1574
|
return None
|
1518
1575
|
|
1519
1576
|
@property
|
@@ -1523,7 +1580,8 @@ class BaseRequest(object):
|
|
1523
1580
|
work if all proxies support the ```X-Forwarded-For`` header. Note
|
1524
1581
|
that this information can be forged by malicious clients. """
|
1525
1582
|
proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
|
1526
|
-
if proxy:
|
1583
|
+
if proxy:
|
1584
|
+
return [ip.strip() for ip in proxy.split(',')]
|
1527
1585
|
remote = self.environ.get('REMOTE_ADDR')
|
1528
1586
|
return [remote] if remote else []
|
1529
1587
|
|
@@ -1589,7 +1647,8 @@ class BaseRequest(object):
|
|
1589
1647
|
|
1590
1648
|
def __setattr__(self, name, value):
|
1591
1649
|
""" Define new attributes that are local to the bound request environment. """
|
1592
|
-
if name == 'environ':
|
1650
|
+
if name == 'environ':
|
1651
|
+
return object.__setattr__(self, name, value)
|
1593
1652
|
key = 'bottle.request.ext.%s' % name
|
1594
1653
|
if hasattr(self, name):
|
1595
1654
|
raise AttributeError("Attribute already defined: %s" % name)
|
@@ -1622,7 +1681,8 @@ class HeaderProperty(object):
|
|
1622
1681
|
self.__doc__ = 'Current value of the %r header.' % name.title()
|
1623
1682
|
|
1624
1683
|
def __get__(self, obj, _):
|
1625
|
-
if obj is None:
|
1684
|
+
if obj is None:
|
1685
|
+
return self
|
1626
1686
|
value = obj.get_header(self.name, self.default)
|
1627
1687
|
return self.reader(value) if self.reader else value
|
1628
1688
|
|
@@ -1649,8 +1709,8 @@ class BaseResponse(object):
|
|
1649
1709
|
bad_headers = {
|
1650
1710
|
204: frozenset(('Content-Type', 'Content-Length')),
|
1651
1711
|
304: frozenset(('Allow', 'Content-Encoding', 'Content-Language',
|
1652
|
-
|
1653
|
-
|
1712
|
+
'Content-Length', 'Content-Range', 'Content-Type',
|
1713
|
+
'Content-Md5', 'Last-Modified'))
|
1654
1714
|
}
|
1655
1715
|
|
1656
1716
|
def __init__(self, body='', status=None, headers=None, **more_headers):
|
@@ -1686,9 +1746,9 @@ class BaseResponse(object):
|
|
1686
1746
|
copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
|
1687
1747
|
if self._cookies:
|
1688
1748
|
cookies = copy._cookies = SimpleCookie()
|
1689
|
-
for k,v in self._cookies.items():
|
1749
|
+
for k, v in self._cookies.items():
|
1690
1750
|
cookies[k] = v.value
|
1691
|
-
cookies[k].update(v)
|
1751
|
+
cookies[k].update(v) # also copy cookie attributes
|
1692
1752
|
return copy
|
1693
1753
|
|
1694
1754
|
def __iter__(self):
|
@@ -1879,13 +1939,13 @@ class BaseResponse(object):
|
|
1879
1939
|
self._cookies[name] = value
|
1880
1940
|
|
1881
1941
|
for key, value in options.items():
|
1882
|
-
if key in ('max_age', 'maxage'):
|
1942
|
+
if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
|
1883
1943
|
key = 'max-age'
|
1884
1944
|
if isinstance(value, timedelta):
|
1885
1945
|
value = value.seconds + value.days * 24 * 3600
|
1886
1946
|
if key == 'expires':
|
1887
1947
|
value = http_date(value)
|
1888
|
-
if key in ('same_site', 'samesite'):
|
1948
|
+
if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
|
1889
1949
|
key, value = 'samesite', (value or "none").lower()
|
1890
1950
|
if value not in ('lax', 'strict', 'none'):
|
1891
1951
|
raise CookieError("Invalid value for SameSite")
|
@@ -2005,19 +2065,20 @@ class JSONPlugin(object):
|
|
2005
2065
|
|
2006
2066
|
def setup(self, app):
|
2007
2067
|
app.config._define('json.enable', default=True, validate=bool,
|
2008
|
-
|
2068
|
+
help="Enable or disable automatic dict->json filter.")
|
2009
2069
|
app.config._define('json.ascii', default=False, validate=bool,
|
2010
|
-
|
2070
|
+
help="Use only 7-bit ASCII characters in output.")
|
2011
2071
|
app.config._define('json.indent', default=True, validate=bool,
|
2012
|
-
|
2072
|
+
help="Add whitespace to make json more readable.")
|
2013
2073
|
app.config._define('json.dump_func', default=None,
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2074
|
+
help="If defined, use this function to transform"
|
2075
|
+
" dict into json. The other options no longer"
|
2076
|
+
" apply.")
|
2017
2077
|
|
2018
2078
|
def apply(self, callback, route):
|
2019
2079
|
dumps = self.json_dumps
|
2020
|
-
if not self.json_dumps:
|
2080
|
+
if not self.json_dumps:
|
2081
|
+
return callback
|
2021
2082
|
|
2022
2083
|
@functools.wraps(callback)
|
2023
2084
|
def wrapper(*a, **ka):
|
@@ -2077,24 +2138,29 @@ class _ImportRedirect(object):
|
|
2077
2138
|
sys.meta_path.append(self)
|
2078
2139
|
|
2079
2140
|
def find_spec(self, fullname, path, target=None):
|
2080
|
-
if '.' not in fullname:
|
2081
|
-
|
2141
|
+
if '.' not in fullname:
|
2142
|
+
return
|
2143
|
+
if fullname.rsplit('.', 1)[0] != self.name:
|
2144
|
+
return
|
2082
2145
|
from importlib.util import spec_from_loader
|
2083
2146
|
return spec_from_loader(fullname, self)
|
2084
2147
|
|
2085
2148
|
def find_module(self, fullname, path=None):
|
2086
|
-
if '.' not in fullname:
|
2087
|
-
|
2149
|
+
if '.' not in fullname:
|
2150
|
+
return
|
2151
|
+
if fullname.rsplit('.', 1)[0] != self.name:
|
2152
|
+
return
|
2088
2153
|
return self
|
2089
2154
|
|
2090
2155
|
def create_module(self, spec):
|
2091
2156
|
return self.load_module(spec.name)
|
2092
2157
|
|
2093
2158
|
def exec_module(self, module):
|
2094
|
-
pass
|
2159
|
+
pass # This probably breaks importlib.reload() :/
|
2095
2160
|
|
2096
2161
|
def load_module(self, fullname):
|
2097
|
-
if fullname in sys.modules:
|
2162
|
+
if fullname in sys.modules:
|
2163
|
+
return sys.modules[fullname]
|
2098
2164
|
modname = fullname.rsplit('.', 1)[1]
|
2099
2165
|
realname = self.impmask % modname
|
2100
2166
|
__import__(realname)
|
@@ -2263,7 +2329,8 @@ class HeaderDict(MultiDict):
|
|
2263
2329
|
|
2264
2330
|
def __init__(self, *a, **ka):
|
2265
2331
|
self.dict = {}
|
2266
|
-
if a or ka:
|
2332
|
+
if a or ka:
|
2333
|
+
self.update(*a, **ka)
|
2267
2334
|
|
2268
2335
|
def __contains__(self, key):
|
2269
2336
|
return _hkey(key) in self.dict
|
@@ -2354,6 +2421,7 @@ class WSGIHeaderDict(DictMixin):
|
|
2354
2421
|
def __contains__(self, key):
|
2355
2422
|
return self._ekey(key) in self.environ
|
2356
2423
|
|
2424
|
+
|
2357
2425
|
_UNSET = object()
|
2358
2426
|
|
2359
2427
|
class ConfigDict(dict):
|
@@ -2535,7 +2603,7 @@ class ConfigDict(dict):
|
|
2535
2603
|
|
2536
2604
|
def meta_set(self, key, metafield, value):
|
2537
2605
|
""" Set the meta field for a key to a new value.
|
2538
|
-
|
2606
|
+
|
2539
2607
|
Meta-fields are shared between all members of an overlay tree.
|
2540
2608
|
"""
|
2541
2609
|
self._meta.setdefault(key, {})[metafield] = value
|
@@ -2595,8 +2663,6 @@ class ConfigDict(dict):
|
|
2595
2663
|
return overlay
|
2596
2664
|
|
2597
2665
|
|
2598
|
-
|
2599
|
-
|
2600
2666
|
class AppStack(list):
|
2601
2667
|
""" A stack-like list. Calling it returns the head of the stack. """
|
2602
2668
|
|
@@ -2624,7 +2690,8 @@ class WSGIFileWrapper(object):
|
|
2624
2690
|
def __init__(self, fp, buffer_size=1024 * 64):
|
2625
2691
|
self.fp, self.buffer_size = fp, buffer_size
|
2626
2692
|
for attr in 'fileno', 'close', 'read', 'readlines', 'tell', 'seek':
|
2627
|
-
if hasattr(fp, attr):
|
2693
|
+
if hasattr(fp, attr):
|
2694
|
+
setattr(self, attr, getattr(fp, attr))
|
2628
2695
|
|
2629
2696
|
def __iter__(self):
|
2630
2697
|
buff, read = self.buffer_size, self.read
|
@@ -2706,11 +2773,14 @@ class ResourceManager(object):
|
|
2706
2773
|
search = self.path[:]
|
2707
2774
|
while search:
|
2708
2775
|
path = search.pop()
|
2709
|
-
if not os.path.isdir(path):
|
2776
|
+
if not os.path.isdir(path):
|
2777
|
+
continue
|
2710
2778
|
for name in os.listdir(path):
|
2711
2779
|
full = os.path.join(path, name)
|
2712
|
-
if os.path.isdir(full):
|
2713
|
-
|
2780
|
+
if os.path.isdir(full):
|
2781
|
+
search.append(full)
|
2782
|
+
else:
|
2783
|
+
yield full
|
2714
2784
|
|
2715
2785
|
def lookup(self, name):
|
2716
2786
|
""" Search for a resource and return an absolute file path, or `None`.
|
@@ -2732,7 +2802,8 @@ class ResourceManager(object):
|
|
2732
2802
|
def open(self, name, mode='r', *args, **kwargs):
|
2733
2803
|
""" Find a resource and return a file object, or raise IOError. """
|
2734
2804
|
fname = self.lookup(name)
|
2735
|
-
if not fname:
|
2805
|
+
if not fname:
|
2806
|
+
raise IOError("Resource %r not found." % name)
|
2736
2807
|
return self.opener(fname, mode=mode, *args, **kwargs)
|
2737
2808
|
|
2738
2809
|
|
@@ -2779,7 +2850,8 @@ class FileUpload(object):
|
|
2779
2850
|
read, write, offset = self.file.read, fp.write, self.file.tell()
|
2780
2851
|
while 1:
|
2781
2852
|
buf = read(chunk_size)
|
2782
|
-
if not buf:
|
2853
|
+
if not buf:
|
2854
|
+
break
|
2783
2855
|
write(buf)
|
2784
2856
|
self.file.seek(offset)
|
2785
2857
|
|
@@ -2889,11 +2961,11 @@ def static_file(filename, root,
|
|
2889
2961
|
mimetype, encoding = mimetypes.guess_type(name)
|
2890
2962
|
if encoding == 'gzip':
|
2891
2963
|
mimetype = 'application/gzip'
|
2892
|
-
elif encoding:
|
2964
|
+
elif encoding: # e.g. bzip2 -> application/x-bzip2
|
2893
2965
|
mimetype = 'application/x-' + encoding
|
2894
2966
|
|
2895
2967
|
if charset and mimetype and 'charset=' not in mimetype \
|
2896
|
-
|
2968
|
+
and (mimetype[:5] == 'text/' or mimetype == 'application/javascript'):
|
2897
2969
|
mimetype += '; charset=%s' % charset
|
2898
2970
|
|
2899
2971
|
if mimetype:
|
@@ -2903,7 +2975,7 @@ def static_file(filename, root,
|
|
2903
2975
|
download = os.path.basename(filename)
|
2904
2976
|
|
2905
2977
|
if download:
|
2906
|
-
download = download.replace('"','')
|
2978
|
+
download = download.replace('"', '')
|
2907
2979
|
headers['Content-Disposition'] = 'attachment; filename="%s"' % download
|
2908
2980
|
|
2909
2981
|
stats = os.stat(filename)
|
@@ -2940,7 +3012,8 @@ def static_file(filename, root,
|
|
2940
3012
|
rlen = end - offset
|
2941
3013
|
headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
|
2942
3014
|
headers["Content-Length"] = str(rlen)
|
2943
|
-
if body:
|
3015
|
+
if body:
|
3016
|
+
body = _closeiter(_rangeiter(body, offset, rlen), body.close)
|
2944
3017
|
return HTTPResponse(body, status=206, **headers)
|
2945
3018
|
return HTTPResponse(body, **headers)
|
2946
3019
|
|
@@ -2953,7 +3026,8 @@ def debug(mode=True):
|
|
2953
3026
|
""" Change the debug level.
|
2954
3027
|
There is only one debug level supported at the moment."""
|
2955
3028
|
global DEBUG
|
2956
|
-
if mode:
|
3029
|
+
if mode:
|
3030
|
+
warnings.simplefilter('default')
|
2957
3031
|
DEBUG = bool(mode)
|
2958
3032
|
|
2959
3033
|
|
@@ -2996,7 +3070,8 @@ def parse_auth(header):
|
|
2996
3070
|
def parse_range_header(header, maxlen=0):
|
2997
3071
|
""" Yield (start, end) ranges parsed from a HTTP Range header. Skip
|
2998
3072
|
unsatisfiable ranges. The end index is non-inclusive."""
|
2999
|
-
if not header or header[:6] != 'bytes=':
|
3073
|
+
if not header or header[:6] != 'bytes=':
|
3074
|
+
return
|
3000
3075
|
ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r]
|
3001
3076
|
for start, end in ranges:
|
3002
3077
|
try:
|
@@ -3051,9 +3126,11 @@ def _parse_http_header(h):
|
|
3051
3126
|
def _parse_qsl(qs):
|
3052
3127
|
r = []
|
3053
3128
|
for pair in qs.split('&'):
|
3054
|
-
if not pair:
|
3129
|
+
if not pair:
|
3130
|
+
continue
|
3055
3131
|
nv = pair.split('=', 1)
|
3056
|
-
if len(nv) != 2:
|
3132
|
+
if len(nv) != 2:
|
3133
|
+
nv.append('')
|
3057
3134
|
key = urlunquote(nv[0].replace('+', ' '))
|
3058
3135
|
value = urlunquote(nv[1].replace('+', ' '))
|
3059
3136
|
r.append((key, value))
|
@@ -3107,7 +3184,7 @@ def html_escape(string):
|
|
3107
3184
|
def html_quote(string):
|
3108
3185
|
""" Escape and quote a string to be used as an HTTP attribute."""
|
3109
3186
|
return '"%s"' % html_escape(string).replace('\n', ' ')\
|
3110
|
-
|
3187
|
+
.replace('\r', ' ').replace('\t', '	')
|
3111
3188
|
|
3112
3189
|
|
3113
3190
|
def yieldroutes(func):
|
@@ -3139,11 +3216,14 @@ def path_shift(script_name, path_info, shift=1):
|
|
3139
3216
|
:param shift: The number of path fragments to shift. May be negative to
|
3140
3217
|
change the shift direction. (default: 1)
|
3141
3218
|
"""
|
3142
|
-
if shift == 0:
|
3219
|
+
if shift == 0:
|
3220
|
+
return script_name, path_info
|
3143
3221
|
pathlist = path_info.strip('/').split('/')
|
3144
3222
|
scriptlist = script_name.strip('/').split('/')
|
3145
|
-
if pathlist and pathlist[0] == '':
|
3146
|
-
|
3223
|
+
if pathlist and pathlist[0] == '':
|
3224
|
+
pathlist = []
|
3225
|
+
if scriptlist and scriptlist[0] == '':
|
3226
|
+
scriptlist = []
|
3147
3227
|
if 0 < shift <= len(pathlist):
|
3148
3228
|
moved = pathlist[:shift]
|
3149
3229
|
scriptlist = scriptlist + moved
|
@@ -3157,7 +3237,8 @@ def path_shift(script_name, path_info, shift=1):
|
|
3157
3237
|
raise AssertionError("Cannot shift. Nothing left from %s" % empty)
|
3158
3238
|
new_script_name = '/' + '/'.join(scriptlist)
|
3159
3239
|
new_path_info = '/' + '/'.join(pathlist)
|
3160
|
-
if path_info.endswith('/') and pathlist:
|
3240
|
+
if path_info.endswith('/') and pathlist:
|
3241
|
+
new_path_info += '/'
|
3161
3242
|
return new_script_name, new_path_info
|
3162
3243
|
|
3163
3244
|
|
@@ -3194,18 +3275,18 @@ def make_default_app_wrapper(name):
|
|
3194
3275
|
return wrapper
|
3195
3276
|
|
3196
3277
|
|
3197
|
-
route
|
3198
|
-
get
|
3199
|
-
post
|
3200
|
-
put
|
3201
|
-
delete
|
3202
|
-
patch
|
3203
|
-
error
|
3204
|
-
mount
|
3205
|
-
hook
|
3206
|
-
install
|
3278
|
+
route = make_default_app_wrapper('route')
|
3279
|
+
get = make_default_app_wrapper('get')
|
3280
|
+
post = make_default_app_wrapper('post')
|
3281
|
+
put = make_default_app_wrapper('put')
|
3282
|
+
delete = make_default_app_wrapper('delete')
|
3283
|
+
patch = make_default_app_wrapper('patch')
|
3284
|
+
error = make_default_app_wrapper('error')
|
3285
|
+
mount = make_default_app_wrapper('mount')
|
3286
|
+
hook = make_default_app_wrapper('hook')
|
3287
|
+
install = make_default_app_wrapper('install')
|
3207
3288
|
uninstall = make_default_app_wrapper('uninstall')
|
3208
|
-
url
|
3289
|
+
url = make_default_app_wrapper('get_url')
|
3209
3290
|
|
3210
3291
|
|
3211
3292
|
###############################################################################
|
@@ -3276,7 +3357,7 @@ class _MultipartParser(object):
|
|
3276
3357
|
if i >= 0:
|
3277
3358
|
yield chunk[scanpos:i], b'\r\n'
|
3278
3359
|
scanpos = i + 2
|
3279
|
-
else:
|
3360
|
+
else: # CRLF not found
|
3280
3361
|
partial = chunk[scanpos:] if scanpos else chunk
|
3281
3362
|
break
|
3282
3363
|
|
@@ -3418,7 +3499,7 @@ class _MultipartPart(object):
|
|
3418
3499
|
if "filename" in self.options:
|
3419
3500
|
self.filename = self.options.get("filename")
|
3420
3501
|
if self.filename[1:3] == ":\\" or self.filename[:2] == "\\\\":
|
3421
|
-
self.filename = self.filename.split("\\")[-1]
|
3502
|
+
self.filename = self.filename.split("\\")[-1] # ie6 bug
|
3422
3503
|
|
3423
3504
|
self.content_type, options = _parse_http_header(content_type)[0] if content_type else (None, {})
|
3424
3505
|
self.charset = options.get("charset") or self.charset
|
@@ -3477,7 +3558,7 @@ class ServerAdapter(object):
|
|
3477
3558
|
|
3478
3559
|
def __repr__(self):
|
3479
3560
|
args = ', '.join('%s=%s' % (k, repr(v))
|
3480
|
-
|
3561
|
+
for k, v in self.options.items())
|
3481
3562
|
return "%s(%s)" % (self.__class__.__name__, args)
|
3482
3563
|
|
3483
3564
|
|
@@ -3503,9 +3584,8 @@ class FlupFCGIServer(ServerAdapter):
|
|
3503
3584
|
|
3504
3585
|
class WSGIRefServer(ServerAdapter):
|
3505
3586
|
def run(self, app): # pragma: no cover
|
3506
|
-
from wsgiref.simple_server import make_server
|
3507
|
-
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer
|
3508
3587
|
import socket
|
3588
|
+
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server
|
3509
3589
|
|
3510
3590
|
class FixedHandler(WSGIRequestHandler):
|
3511
3591
|
def address_string(self): # Prevent reverse DNS lookups please.
|
@@ -3539,7 +3619,7 @@ class CherryPyServer(ServerAdapter):
|
|
3539
3619
|
depr(0, 13, "The wsgi server part of cherrypy was split into a new "
|
3540
3620
|
"project called 'cheroot'.", "Use the 'cheroot' server "
|
3541
3621
|
"adapter instead of cherrypy.")
|
3542
|
-
from cherrypy import wsgiserver
|
3622
|
+
from cherrypy import wsgiserver # This will fail for CherryPy >= 9
|
3543
3623
|
|
3544
3624
|
self.options['bind_addr'] = (self.host, self.port)
|
3545
3625
|
self.options['wsgi_app'] = handler
|
@@ -3564,7 +3644,7 @@ class CherryPyServer(ServerAdapter):
|
|
3564
3644
|
|
3565
3645
|
|
3566
3646
|
class CherootServer(ServerAdapter):
|
3567
|
-
def run(self, handler):
|
3647
|
+
def run(self, handler): # pragma: no cover
|
3568
3648
|
from cheroot import wsgi
|
3569
3649
|
from cheroot.ssl import builtin
|
3570
3650
|
self.options['bind_addr'] = (self.host, self.port)
|
@@ -3575,7 +3655,7 @@ class CherootServer(ServerAdapter):
|
|
3575
3655
|
server = wsgi.Server(**self.options)
|
3576
3656
|
if certfile and keyfile:
|
3577
3657
|
server.ssl_adapter = builtin.BuiltinSSLAdapter(
|
3578
|
-
|
3658
|
+
certfile, keyfile, chainfile)
|
3579
3659
|
try:
|
3580
3660
|
server.start()
|
3581
3661
|
finally:
|
@@ -3635,7 +3715,9 @@ class TornadoServer(ServerAdapter):
|
|
3635
3715
|
""" The super hyped asynchronous server by facebook. Untested. """
|
3636
3716
|
|
3637
3717
|
def run(self, handler): # pragma: no cover
|
3638
|
-
import tornado.
|
3718
|
+
import tornado.httpserver
|
3719
|
+
import tornado.ioloop
|
3720
|
+
import tornado.wsgi
|
3639
3721
|
container = tornado.wsgi.WSGIContainer(handler)
|
3640
3722
|
server = tornado.httpserver.HTTPServer(container)
|
3641
3723
|
server.listen(port=self.port, address=self.host)
|
@@ -3650,6 +3732,7 @@ class AppEngineServer(ServerAdapter):
|
|
3650
3732
|
depr(0, 13, "AppEngineServer no longer required",
|
3651
3733
|
"Configure your application directly in your app.yaml")
|
3652
3734
|
from google.appengine.ext.webapp import util
|
3735
|
+
|
3653
3736
|
# A main() function in the handler script enables 'App Caching'.
|
3654
3737
|
# Lets makes sure it is there. This _really_ improves performance.
|
3655
3738
|
module = sys.modules.get('__main__')
|
@@ -3662,9 +3745,9 @@ class TwistedServer(ServerAdapter):
|
|
3662
3745
|
""" Untested. """
|
3663
3746
|
|
3664
3747
|
def run(self, handler):
|
3665
|
-
from twisted.web import server, wsgi
|
3666
|
-
from twisted.python.threadpool import ThreadPool
|
3667
3748
|
from twisted.internet import reactor
|
3749
|
+
from twisted.python.threadpool import ThreadPool
|
3750
|
+
from twisted.web import server, wsgi
|
3668
3751
|
thread_pool = ThreadPool()
|
3669
3752
|
thread_pool.start()
|
3670
3753
|
reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
|
@@ -3691,7 +3774,7 @@ class GeventServer(ServerAdapter):
|
|
3691
3774
|
"""
|
3692
3775
|
|
3693
3776
|
def run(self, handler):
|
3694
|
-
from gevent import
|
3777
|
+
from gevent import local, pywsgi
|
3695
3778
|
if not isinstance(threading.local(), local.local):
|
3696
3779
|
msg = "Bottle requires gevent.monkey.patch_all() (before import)"
|
3697
3780
|
raise RuntimeError(msg)
|
@@ -3740,7 +3823,7 @@ class EventletServer(ServerAdapter):
|
|
3740
3823
|
"""
|
3741
3824
|
|
3742
3825
|
def run(self, handler):
|
3743
|
-
from eventlet import
|
3826
|
+
from eventlet import listen, patcher, wsgi
|
3744
3827
|
if not patcher.is_monkey_patched(os):
|
3745
3828
|
msg = "Bottle requires eventlet.monkey_patch() (before import)"
|
3746
3829
|
raise RuntimeError(msg)
|
@@ -3768,6 +3851,7 @@ class BjoernServer(ServerAdapter):
|
|
3768
3851
|
|
3769
3852
|
class AsyncioServerAdapter(ServerAdapter):
|
3770
3853
|
""" Extend ServerAdapter for adding custom event loop """
|
3854
|
+
|
3771
3855
|
def get_event_loop(self):
|
3772
3856
|
pass
|
3773
3857
|
|
@@ -3783,6 +3867,7 @@ class AiohttpServer(AsyncioServerAdapter):
|
|
3783
3867
|
|
3784
3868
|
def run(self, handler):
|
3785
3869
|
import asyncio
|
3870
|
+
|
3786
3871
|
from aiohttp_wsgi.wsgi import serve
|
3787
3872
|
self.loop = self.get_event_loop()
|
3788
3873
|
asyncio.set_event_loop(self.loop)
|
@@ -3798,6 +3883,7 @@ class AiohttpUVLoopServer(AiohttpServer):
|
|
3798
3883
|
"""uvloop
|
3799
3884
|
https://github.com/MagicStack/uvloop
|
3800
3885
|
"""
|
3886
|
+
|
3801
3887
|
def get_event_loop(self):
|
3802
3888
|
import uvloop
|
3803
3889
|
return uvloop.new_event_loop()
|
@@ -3855,9 +3941,12 @@ def load(target, **namespace):
|
|
3855
3941
|
local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
|
3856
3942
|
"""
|
3857
3943
|
module, target = target.split(":", 1) if ':' in target else (target, None)
|
3858
|
-
if module not in sys.modules:
|
3859
|
-
|
3860
|
-
if target
|
3944
|
+
if module not in sys.modules:
|
3945
|
+
__import__(module)
|
3946
|
+
if not target:
|
3947
|
+
return sys.modules[module]
|
3948
|
+
if target.isalnum():
|
3949
|
+
return getattr(sys.modules[module], target)
|
3861
3950
|
package_name = module.split('.')[0]
|
3862
3951
|
namespace[package_name] = sys.modules[package_name]
|
3863
3952
|
return eval('%s.%s' % (module, target), namespace)
|
@@ -3907,7 +3996,8 @@ def run(app=None,
|
|
3907
3996
|
:param quiet: Suppress output to stdout and stderr? (default: False)
|
3908
3997
|
:param options: Options passed to the server adapter.
|
3909
3998
|
"""
|
3910
|
-
if NORUN:
|
3999
|
+
if NORUN:
|
4000
|
+
return
|
3911
4001
|
if reloader and not os.environ.get('BOTTLE_CHILD'):
|
3912
4002
|
import subprocess
|
3913
4003
|
fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
|
@@ -3938,7 +4028,8 @@ def run(app=None,
|
|
3938
4028
|
return
|
3939
4029
|
|
3940
4030
|
try:
|
3941
|
-
if debug is not None:
|
4031
|
+
if debug is not None:
|
4032
|
+
_debug(debug)
|
3942
4033
|
app = app or default_app()
|
3943
4034
|
if isinstance(app, basestring):
|
3944
4035
|
app = load_app(app)
|
@@ -3987,7 +4078,8 @@ def run(app=None,
|
|
3987
4078
|
except (SystemExit, MemoryError):
|
3988
4079
|
raise
|
3989
4080
|
except:
|
3990
|
-
if not reloader:
|
4081
|
+
if not reloader:
|
4082
|
+
raise
|
3991
4083
|
if not getattr(server, 'quiet', quiet):
|
3992
4084
|
print_exc()
|
3993
4085
|
time.sleep(interval)
|
@@ -4007,17 +4099,19 @@ class FileCheckerThread(threading.Thread):
|
|
4007
4099
|
|
4008
4100
|
def run(self):
|
4009
4101
|
exists = os.path.exists
|
4010
|
-
mtime
|
4102
|
+
def mtime(p): return os.stat(p).st_mtime
|
4011
4103
|
files = dict()
|
4012
4104
|
|
4013
4105
|
for module in list(sys.modules.values()):
|
4014
4106
|
path = getattr(module, '__file__', '') or ''
|
4015
|
-
if path[-4:] in ('.pyo', '.pyc'):
|
4016
|
-
|
4107
|
+
if path[-4:] in ('.pyo', '.pyc'):
|
4108
|
+
path = path[:-1]
|
4109
|
+
if path and exists(path):
|
4110
|
+
files[path] = mtime(path)
|
4017
4111
|
|
4018
4112
|
while not self.status:
|
4019
4113
|
if not exists(self.lockfile)\
|
4020
|
-
|
4114
|
+
or mtime(self.lockfile) < time.time() - self.interval - 5:
|
4021
4115
|
self.status = 'error'
|
4022
4116
|
thread.interrupt_main()
|
4023
4117
|
for path, lmtime in list(files.items()):
|
@@ -4031,7 +4125,8 @@ class FileCheckerThread(threading.Thread):
|
|
4031
4125
|
self.start()
|
4032
4126
|
|
4033
4127
|
def __exit__(self, exc_type, *_):
|
4034
|
-
if not self.status:
|
4128
|
+
if not self.status:
|
4129
|
+
self.status = 'exit' # silent exit
|
4035
4130
|
self.join()
|
4036
4131
|
return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
|
4037
4132
|
|
@@ -4047,8 +4142,8 @@ class TemplateError(BottleException):
|
|
4047
4142
|
class BaseTemplate(object):
|
4048
4143
|
""" Base class and minimal API for template adapters """
|
4049
4144
|
extensions = ['tpl', 'html', 'thtml', 'stpl']
|
4050
|
-
settings = {} #used in prepare()
|
4051
|
-
defaults = {} #used in render()
|
4145
|
+
settings = {} # used in prepare()
|
4146
|
+
defaults = {} # used in render()
|
4052
4147
|
|
4053
4148
|
def __init__(self,
|
4054
4149
|
source=None,
|
@@ -4094,8 +4189,10 @@ class BaseTemplate(object):
|
|
4094
4189
|
for spath in lookup:
|
4095
4190
|
spath = os.path.abspath(spath) + os.sep
|
4096
4191
|
fname = os.path.abspath(os.path.join(spath, name))
|
4097
|
-
if not fname.startswith(spath):
|
4098
|
-
|
4192
|
+
if not fname.startswith(spath):
|
4193
|
+
continue
|
4194
|
+
if os.path.isfile(fname):
|
4195
|
+
return fname
|
4099
4196
|
for ext in cls.extensions:
|
4100
4197
|
if os.path.isfile('%s.%s' % (fname, ext)):
|
4101
4198
|
return '%s.%s' % (fname, ext)
|
@@ -4128,8 +4225,8 @@ class BaseTemplate(object):
|
|
4128
4225
|
|
4129
4226
|
class MakoTemplate(BaseTemplate):
|
4130
4227
|
def prepare(self, **options):
|
4131
|
-
from mako.template import Template
|
4132
4228
|
from mako.lookup import TemplateLookup
|
4229
|
+
from mako.template import Template
|
4133
4230
|
options.update({'input_encoding': self.encoding})
|
4134
4231
|
options.setdefault('format_exceptions', bool(DEBUG))
|
4135
4232
|
lookup = TemplateLookup(directories=self.lookup, **options)
|
@@ -4173,9 +4270,12 @@ class Jinja2Template(BaseTemplate):
|
|
4173
4270
|
def prepare(self, filters=None, tests=None, globals={}, **kwargs):
|
4174
4271
|
from jinja2 import Environment, FunctionLoader
|
4175
4272
|
self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
|
4176
|
-
if filters:
|
4177
|
-
|
4178
|
-
if
|
4273
|
+
if filters:
|
4274
|
+
self.env.filters.update(filters)
|
4275
|
+
if tests:
|
4276
|
+
self.env.tests.update(tests)
|
4277
|
+
if globals:
|
4278
|
+
self.env.globals.update(globals)
|
4179
4279
|
if self.source:
|
4180
4280
|
self.tpl = self.env.from_string(self.source)
|
4181
4281
|
else:
|
@@ -4193,7 +4293,8 @@ class Jinja2Template(BaseTemplate):
|
|
4193
4293
|
fname = name
|
4194
4294
|
else:
|
4195
4295
|
fname = self.search(name, self.lookup)
|
4196
|
-
if not fname:
|
4296
|
+
if not fname:
|
4297
|
+
return
|
4197
4298
|
with open(fname, "rb") as f:
|
4198
4299
|
return (f.read().decode(self.encoding), fname, lambda: False)
|
4199
4300
|
|
@@ -4258,7 +4359,7 @@ class SimpleTemplate(BaseTemplate):
|
|
4258
4359
|
exec(self.co, env)
|
4259
4360
|
if env.get('_rebase'):
|
4260
4361
|
subtpl, rargs = env.pop('_rebase')
|
4261
|
-
rargs['base'] = ''.join(_stdout) #copy stdout
|
4362
|
+
rargs['base'] = ''.join(_stdout) # copy stdout
|
4262
4363
|
del _stdout[:] # clear stdout
|
4263
4364
|
return self._include(env, subtpl, **rargs)
|
4264
4365
|
return env
|
@@ -4332,7 +4433,6 @@ class StplParser(object):
|
|
4332
4433
|
_re_tok = '(?mx)' + _re_tok
|
4333
4434
|
_re_inl = '(?mx)' + _re_inl
|
4334
4435
|
|
4335
|
-
|
4336
4436
|
default_syntax = '<% %> % {{ }}'
|
4337
4437
|
|
4338
4438
|
def __init__(self, source, syntax=None, encoding='utf8'):
|
@@ -4362,7 +4462,8 @@ class StplParser(object):
|
|
4362
4462
|
syntax = property(get_syntax, set_syntax)
|
4363
4463
|
|
4364
4464
|
def translate(self):
|
4365
|
-
if self.offset:
|
4465
|
+
if self.offset:
|
4466
|
+
raise RuntimeError('Parser is a one time instance.')
|
4366
4467
|
while True:
|
4367
4468
|
m = self.re_split.search(self.source, pos=self.offset)
|
4368
4469
|
if m:
|
@@ -4423,8 +4524,10 @@ class StplParser(object):
|
|
4423
4524
|
code_line = _blk2
|
4424
4525
|
self.indent_mod -= 1
|
4425
4526
|
elif _cend: # The end-code-block template token (usually '%>')
|
4426
|
-
if multiline:
|
4427
|
-
|
4527
|
+
if multiline:
|
4528
|
+
multiline = False
|
4529
|
+
else:
|
4530
|
+
code_line += _cend
|
4428
4531
|
elif _end:
|
4429
4532
|
self.indent -= 1
|
4430
4533
|
self.indent_mod += 1
|
@@ -4440,19 +4543,23 @@ class StplParser(object):
|
|
4440
4543
|
def flush_text(self):
|
4441
4544
|
text = ''.join(self.text_buffer)
|
4442
4545
|
del self.text_buffer[:]
|
4443
|
-
if not text:
|
4546
|
+
if not text:
|
4547
|
+
return
|
4444
4548
|
parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent
|
4445
4549
|
for m in self.re_inl.finditer(text):
|
4446
4550
|
prefix, pos = text[pos:m.start()], m.end()
|
4447
4551
|
if prefix:
|
4448
4552
|
parts.append(nl.join(map(repr, prefix.splitlines(True))))
|
4449
|
-
if prefix.endswith('\n'):
|
4553
|
+
if prefix.endswith('\n'):
|
4554
|
+
parts[-1] += nl
|
4450
4555
|
parts.append(self.process_inline(m.group(1).strip()))
|
4451
4556
|
if pos < len(text):
|
4452
4557
|
prefix = text[pos:]
|
4453
4558
|
lines = prefix.splitlines(True)
|
4454
|
-
if lines[-1].endswith('\\\\\n'):
|
4455
|
-
|
4559
|
+
if lines[-1].endswith('\\\\\n'):
|
4560
|
+
lines[-1] = lines[-1][:-3]
|
4561
|
+
elif lines[-1].endswith('\\\\\r\n'):
|
4562
|
+
lines[-1] = lines[-1][:-4]
|
4456
4563
|
parts.append(nl.join(map(repr, lines)))
|
4457
4564
|
code = '_printlist((%s,))' % ', '.join(parts)
|
4458
4565
|
self.lineno += code.count('\n') + 1
|
@@ -4460,7 +4567,8 @@ class StplParser(object):
|
|
4460
4567
|
|
4461
4568
|
@staticmethod
|
4462
4569
|
def process_inline(chunk):
|
4463
|
-
if chunk[0] == '!':
|
4570
|
+
if chunk[0] == '!':
|
4571
|
+
return '_str(%s)' % chunk[1:]
|
4464
4572
|
return '_escape(%s)' % chunk
|
4465
4573
|
|
4466
4574
|
def write_code(self, line, comment=''):
|
@@ -4486,7 +4594,8 @@ def template(*args, **kwargs):
|
|
4486
4594
|
settings = kwargs.pop('template_settings', {})
|
4487
4595
|
if isinstance(tpl, adapter):
|
4488
4596
|
TEMPLATES[tplid] = tpl
|
4489
|
-
if settings:
|
4597
|
+
if settings:
|
4598
|
+
TEMPLATES[tplid].prepare(**settings)
|
4490
4599
|
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
|
4491
4600
|
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
|
4492
4601
|
else:
|
@@ -4550,7 +4659,7 @@ HTTP_CODES[418] = "I'm a teapot" # RFC 2324
|
|
4550
4659
|
HTTP_CODES[428] = "Precondition Required"
|
4551
4660
|
HTTP_CODES[429] = "Too Many Requests"
|
4552
4661
|
HTTP_CODES[431] = "Request Header Fields Too Large"
|
4553
|
-
HTTP_CODES[451] = "Unavailable For Legal Reasons"
|
4662
|
+
HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725
|
4554
4663
|
HTTP_CODES[511] = "Network Authentication Required"
|
4555
4664
|
_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v))
|
4556
4665
|
for (k, v) in HTTP_CODES.items())
|
@@ -4614,7 +4723,7 @@ apps = app = default_app = AppStack()
|
|
4614
4723
|
|
4615
4724
|
#: A virtual package that redirects import statements.
|
4616
4725
|
#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
|
4617
|
-
ext = _ImportRedirect('bottle.ext' if __name__ ==
|
4726
|
+
ext = _ImportRedirect('bottle.ext' if __name__ == "__main__" else
|
4618
4727
|
__name__ + ".ext", 'bottle_%s').module
|
4619
4728
|
|
4620
4729
|
|
@@ -4676,5 +4785,5 @@ def main():
|
|
4676
4785
|
_main(sys.argv)
|
4677
4786
|
|
4678
4787
|
|
4679
|
-
if __name__ ==
|
4788
|
+
if __name__ == "__main__": # pragma: no coverage
|
4680
4789
|
main()
|
ezKit/utils.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
"""Utils"""
|
2
2
|
import csv
|
3
3
|
import hashlib
|
4
|
+
import importlib
|
5
|
+
import importlib.util
|
4
6
|
import json
|
5
7
|
import os
|
6
8
|
import re
|
7
9
|
import subprocess
|
8
10
|
import time
|
9
11
|
import tomllib
|
10
|
-
from ast import literal_eval
|
11
12
|
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
12
13
|
from copy import deepcopy
|
13
14
|
from datetime import date, datetime, timedelta, timezone
|
@@ -18,9 +19,11 @@ from typing import Any, Callable
|
|
18
19
|
from urllib.parse import ParseResult, urlparse
|
19
20
|
from uuid import uuid4
|
20
21
|
|
21
|
-
import markdown
|
22
22
|
from loguru import logger
|
23
23
|
|
24
|
+
if importlib.util.find_spec("markdown"):
|
25
|
+
import markdown # type: ignore
|
26
|
+
|
24
27
|
# --------------------------------------------------------------------------------------------------
|
25
28
|
|
26
29
|
|
@@ -1408,21 +1411,24 @@ def git_clone(
|
|
1408
1411
|
|
1409
1412
|
|
1410
1413
|
def url_parse(
|
1411
|
-
url: str
|
1412
|
-
scheme: str =
|
1413
|
-
) -> ParseResult:
|
1414
|
+
url: str
|
1415
|
+
# scheme: str = "http"
|
1416
|
+
) -> ParseResult | None:
|
1414
1417
|
"""URL Parse"""
|
1415
|
-
none_result = ParseResult(scheme='', netloc='', path='', params='', query='', fragment='')
|
1418
|
+
# none_result = ParseResult(scheme='', netloc='', path='', params='', query='', fragment='')
|
1416
1419
|
try:
|
1420
|
+
if not check_arguments([(url, str, "url_parse -> url")]):
|
1421
|
+
return None
|
1417
1422
|
# 如果没有 scheme 的话, 字符串是不解析的. 所以, 如果没有 scheme, 就添加一个 scheme, 默认添加 http
|
1418
|
-
if isTrue(url, str) and (url.find('://') == -1) and isTrue(scheme, str):
|
1419
|
-
|
1420
|
-
if isTrue(url, str):
|
1421
|
-
|
1422
|
-
return
|
1423
|
+
# if isTrue(url, str) and (url.find('://') == -1) and isTrue(scheme, str):
|
1424
|
+
# url = f'{scheme}://{url}'
|
1425
|
+
# if isTrue(url, str):
|
1426
|
+
# return urlparse(url)
|
1427
|
+
# return None
|
1428
|
+
return urlparse(url)
|
1423
1429
|
except Exception as e:
|
1424
1430
|
logger.exception(e)
|
1425
|
-
return
|
1431
|
+
return None
|
1426
1432
|
|
1427
1433
|
# def debug_log(
|
1428
1434
|
# log: None | str = None,
|
@@ -1476,7 +1482,9 @@ def markdown_to_html(markdown_file: str, html_file: str, title: str) -> bool:
|
|
1476
1482
|
|
1477
1483
|
# 将 Markdown 转换为 HTML
|
1478
1484
|
logger.info(f"{info} [将 Markdown 转换为 HTML]")
|
1479
|
-
|
1485
|
+
# pylint: disable=E0606
|
1486
|
+
html_body = markdown.markdown(markdown_content, extensions=['tables']) # type: ignore
|
1487
|
+
# pylint: enable=E0606
|
1480
1488
|
|
1481
1489
|
# 构造完整的 HTML
|
1482
1490
|
logger.info(f"{info} [构造完整的 HTML]")
|
@@ -1,9 +1,14 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: ezKit
|
3
|
-
Version: 1.11.
|
3
|
+
Version: 1.11.15
|
4
4
|
Summary: Easy Kit
|
5
5
|
Author: septvean
|
6
6
|
Author-email: septvean@gmail.com
|
7
7
|
Requires-Python: >=3.11
|
8
8
|
License-File: LICENSE
|
9
9
|
Requires-Dist: loguru>=0.7
|
10
|
+
Dynamic: author
|
11
|
+
Dynamic: author-email
|
12
|
+
Dynamic: requires-dist
|
13
|
+
Dynamic: requires-python
|
14
|
+
Dynamic: summary
|
@@ -1,6 +1,6 @@
|
|
1
1
|
ezKit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
ezKit/_file.py,sha256=0qRZhwYuagTgTGrhm-tzAMvEQT4HTJA_xZKqF2bo0ho,1207
|
3
|
-
ezKit/bottle.py,sha256=
|
3
|
+
ezKit/bottle.py,sha256=43h4v1kzz6qrLvCt5IMN0H-gFtaT0koG9wETqteXsps,181666
|
4
4
|
ezKit/bottle_extensions.py,sha256=CwXKxVKxxtbyfeeOSp2xODUqJBo7ro2C88H9sUOVDJI,1161
|
5
5
|
ezKit/cipher.py,sha256=0T_StbjiNI4zgrjVgcfU-ffKgu1waBA9UDudAnqFcNM,2896
|
6
6
|
ezKit/database.py,sha256=r5YNoEzeOeVTlEWI99xXtHTmPZ73_DopS8DTzZk8Lts,12432
|
@@ -11,10 +11,10 @@ ezKit/qywx.py,sha256=dGChIIf2V81MwufcPn6hwgSenPuxqK994KRH7ECT-CM,10387
|
|
11
11
|
ezKit/redis.py,sha256=tdiqfizPYQQTIUumkJGUJsJVlv0zVTSTYGQN0QutYs4,1963
|
12
12
|
ezKit/sendemail.py,sha256=47JTDFoLJKi0YtF3RAp9nFfo0ko2jlde3R_C1wr2E2w,7397
|
13
13
|
ezKit/token.py,sha256=HKREyZj_T2S8-aFoFIrBXTaCKExQq4zE66OHXhGHqQg,1750
|
14
|
-
ezKit/utils.py,sha256=
|
14
|
+
ezKit/utils.py,sha256=uOUOCgx6WU6J2lTbHlL78Flk3oCZgdj8rBOFg2i0K7Q,44241
|
15
15
|
ezKit/xftp.py,sha256=izUH9pLH_AzgR3c0g8xSfhLn7LQ9EDcTst3LFjTM6hU,7878
|
16
|
-
ezKit-1.11.
|
17
|
-
ezKit-1.11.
|
18
|
-
ezKit-1.11.
|
19
|
-
ezKit-1.11.
|
20
|
-
ezKit-1.11.
|
16
|
+
ezKit-1.11.15.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
17
|
+
ezKit-1.11.15.dist-info/METADATA,sha256=GyLTqpXZ0J06gOrmQemznFBaU7CNDmJLT7izboiDSWQ,295
|
18
|
+
ezKit-1.11.15.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
19
|
+
ezKit-1.11.15.dist-info/top_level.txt,sha256=aYLB_1WODsqNTsTFWcKP-BN0KCTKcV-HZJ4zlHkCFw8,6
|
20
|
+
ezKit-1.11.15.dist-info/RECORD,,
|
File without changes
|
File without changes
|