ezKit 1.11.13__py3-none-any.whl → 1.11.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|