fresco 3.2.0__py3-none-any.whl → 3.3.1__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.
- fresco/__init__.py +110 -9
- fresco/core.py +10 -8
- fresco/middleware.py +1 -1
- fresco/multidict.py +51 -34
- fresco/options.py +131 -21
- fresco/request.py +1 -1
- fresco/requestcontext.py +16 -9
- fresco/response.py +24 -23
- fresco/routing.py +41 -10
- fresco/subrequests.py +3 -2
- fresco/util/http.py +2 -1
- {fresco-3.2.0.dist-info → fresco-3.3.1.dist-info}/METADATA +1 -1
- {fresco-3.2.0.dist-info → fresco-3.3.1.dist-info}/RECORD +16 -16
- {fresco-3.2.0.dist-info → fresco-3.3.1.dist-info}/LICENSE.txt +0 -0
- {fresco-3.2.0.dist-info → fresco-3.3.1.dist-info}/WHEEL +0 -0
- {fresco-3.2.0.dist-info → fresco-3.3.1.dist-info}/top_level.txt +0 -0
fresco/__init__.py
CHANGED
|
@@ -12,16 +12,117 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
#
|
|
15
|
-
__version__ = "3.
|
|
15
|
+
__version__ = "3.3.1"
|
|
16
16
|
|
|
17
17
|
DEFAULT_CHARSET = "UTF-8"
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Request",
|
|
21
|
+
"currentrequest",
|
|
22
|
+
"context",
|
|
23
|
+
"Response",
|
|
24
|
+
"FrescoApp",
|
|
25
|
+
"urlfor",
|
|
26
|
+
"ALL_METHODS",
|
|
27
|
+
"DEFAULT_CHARSET",
|
|
28
|
+
"GET",
|
|
29
|
+
"HEAD",
|
|
30
|
+
"POST",
|
|
31
|
+
"PUT",
|
|
32
|
+
"DELETE",
|
|
33
|
+
"OPTIONS",
|
|
34
|
+
"TRACE",
|
|
35
|
+
"CONNECT",
|
|
36
|
+
"VERSION_CONTROL",
|
|
37
|
+
"REPORT",
|
|
38
|
+
"CHECKOUT",
|
|
39
|
+
"CHECKIN",
|
|
40
|
+
"UNCHECKOUT",
|
|
41
|
+
"MKWORKSPACE",
|
|
42
|
+
"UPDATE",
|
|
43
|
+
"LABEL",
|
|
44
|
+
"MERGE",
|
|
45
|
+
"BASELINE_CONTROL",
|
|
46
|
+
"MKACTIVITY",
|
|
47
|
+
"ORDERPATCH",
|
|
48
|
+
"ACL",
|
|
49
|
+
"SEARCH",
|
|
50
|
+
"PATCH",
|
|
51
|
+
"DelegateRoute",
|
|
52
|
+
"Options",
|
|
53
|
+
"Route",
|
|
54
|
+
"RouteCollection",
|
|
55
|
+
"RRoute",
|
|
56
|
+
"routearg",
|
|
57
|
+
"FormArg",
|
|
58
|
+
"PostArg",
|
|
59
|
+
"QueryArg",
|
|
60
|
+
"GetArg",
|
|
61
|
+
"CookieArg",
|
|
62
|
+
"SessionArg",
|
|
63
|
+
"RequestObject",
|
|
64
|
+
"FormData",
|
|
65
|
+
"PostData",
|
|
66
|
+
"QueryData",
|
|
67
|
+
"GetData",
|
|
68
|
+
"routefor",
|
|
69
|
+
"XForwarded",
|
|
70
|
+
"subrequest",
|
|
71
|
+
"subrequest_bytes",
|
|
72
|
+
"subrequest_raw",
|
|
73
|
+
"object_or_404",
|
|
74
|
+
]
|
|
26
75
|
|
|
27
|
-
from .
|
|
76
|
+
from fresco.request import Request
|
|
77
|
+
from fresco.request import currentrequest
|
|
78
|
+
from fresco.requestcontext import context
|
|
79
|
+
from fresco.response import Response
|
|
80
|
+
from fresco.core import FrescoApp
|
|
81
|
+
from fresco.core import urlfor
|
|
82
|
+
from fresco.options import Options
|
|
83
|
+
from fresco.routing import Route
|
|
84
|
+
from fresco.routing import RRoute
|
|
85
|
+
from fresco.routing import RouteCollection
|
|
86
|
+
from fresco.routing import DelegateRoute
|
|
87
|
+
from fresco.routing import routefor
|
|
88
|
+
from fresco.routing import ALL_METHODS
|
|
89
|
+
from fresco.routing import GET
|
|
90
|
+
from fresco.routing import HEAD
|
|
91
|
+
from fresco.routing import POST
|
|
92
|
+
from fresco.routing import PUT
|
|
93
|
+
from fresco.routing import DELETE
|
|
94
|
+
from fresco.routing import OPTIONS
|
|
95
|
+
from fresco.routing import TRACE
|
|
96
|
+
from fresco.routing import CONNECT
|
|
97
|
+
from fresco.routing import VERSION_CONTROL
|
|
98
|
+
from fresco.routing import REPORT
|
|
99
|
+
from fresco.routing import CHECKOUT
|
|
100
|
+
from fresco.routing import CHECKIN
|
|
101
|
+
from fresco.routing import UNCHECKOUT
|
|
102
|
+
from fresco.routing import MKWORKSPACE
|
|
103
|
+
from fresco.routing import UPDATE
|
|
104
|
+
from fresco.routing import LABEL
|
|
105
|
+
from fresco.routing import MERGE
|
|
106
|
+
from fresco.routing import BASELINE_CONTROL
|
|
107
|
+
from fresco.routing import MKACTIVITY
|
|
108
|
+
from fresco.routing import ORDERPATCH
|
|
109
|
+
from fresco.routing import ACL
|
|
110
|
+
from fresco.routing import SEARCH
|
|
111
|
+
from fresco.routing import PATCH
|
|
112
|
+
from fresco.routeargs import routearg
|
|
113
|
+
from fresco.routeargs import FormArg
|
|
114
|
+
from fresco.routeargs import PostArg
|
|
115
|
+
from fresco.routeargs import QueryArg
|
|
116
|
+
from fresco.routeargs import GetArg
|
|
117
|
+
from fresco.routeargs import CookieArg
|
|
118
|
+
from fresco.routeargs import SessionArg
|
|
119
|
+
from fresco.routeargs import RequestObject
|
|
120
|
+
from fresco.routeargs import FormData
|
|
121
|
+
from fresco.routeargs import PostData
|
|
122
|
+
from fresco.routeargs import QueryData
|
|
123
|
+
from fresco.routeargs import GetData
|
|
124
|
+
from fresco.middleware import XForwarded
|
|
125
|
+
from fresco.subrequests import subrequest
|
|
126
|
+
from fresco.subrequests import subrequest_bytes
|
|
127
|
+
from fresco.subrequests import subrequest_raw
|
|
128
|
+
from fresco.util.common import object_or_404
|
fresco/core.py
CHANGED
|
@@ -151,9 +151,9 @@ class FrescoApp(RouteCollection):
|
|
|
151
151
|
|
|
152
152
|
def get_response(
|
|
153
153
|
self,
|
|
154
|
-
request,
|
|
155
|
-
path,
|
|
156
|
-
method,
|
|
154
|
+
request: Request,
|
|
155
|
+
path: str,
|
|
156
|
+
method: str,
|
|
157
157
|
currentcontext=context.currentcontext,
|
|
158
158
|
normpath=normpath,
|
|
159
159
|
):
|
|
@@ -259,12 +259,12 @@ class FrescoApp(RouteCollection):
|
|
|
259
259
|
|
|
260
260
|
# Is the URL just missing a trailing '/'?
|
|
261
261
|
if not path or path[-1] != "/":
|
|
262
|
-
|
|
262
|
+
if self.get_methods(request, path + "/"):
|
|
263
263
|
return Response.unrestricted_redirect_permanent(path + "/")
|
|
264
264
|
|
|
265
265
|
return Response.not_found()
|
|
266
266
|
|
|
267
|
-
def view(self, request=None) -> Response:
|
|
267
|
+
def view(self, request: t.Optional[Request] = None) -> Response:
|
|
268
268
|
request = request or context.request
|
|
269
269
|
try:
|
|
270
270
|
path = request.path_info
|
|
@@ -287,7 +287,9 @@ class FrescoApp(RouteCollection):
|
|
|
287
287
|
|
|
288
288
|
return response
|
|
289
289
|
|
|
290
|
-
def handle_http_error_response(
|
|
290
|
+
def handle_http_error_response(
|
|
291
|
+
self, request: Request, response: Response
|
|
292
|
+
) -> Response:
|
|
291
293
|
"""
|
|
292
294
|
Call any process_http_error_response handlers and return the
|
|
293
295
|
(potentially modified) response object.
|
|
@@ -303,8 +305,8 @@ class FrescoApp(RouteCollection):
|
|
|
303
305
|
self.log_exception(request)
|
|
304
306
|
return response
|
|
305
307
|
|
|
306
|
-
def get_methods(self, request, path):
|
|
307
|
-
"""
|
|
308
|
+
def get_methods(self, request: Request, path: str) -> Set[str]:
|
|
309
|
+
"""
|
|
308
310
|
Return the HTTP methods valid in routes to the given path
|
|
309
311
|
"""
|
|
310
312
|
methods: Set[str] = set()
|
fresco/middleware.py
CHANGED
fresco/multidict.py
CHANGED
|
@@ -16,12 +16,24 @@
|
|
|
16
16
|
An order preserving multidict implementation
|
|
17
17
|
"""
|
|
18
18
|
from collections.abc import MutableMapping
|
|
19
|
+
from collections.abc import Mapping
|
|
19
20
|
from itertools import repeat
|
|
21
|
+
import typing as t
|
|
20
22
|
from typing import Any
|
|
21
23
|
from typing import Dict
|
|
22
24
|
from typing import Set
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
if t.TYPE_CHECKING:
|
|
28
|
+
from typeshed import SupportsKeysAndGetItem
|
|
29
|
+
else:
|
|
30
|
+
SupportsKeysAndGetItem = Mapping
|
|
31
|
+
|
|
32
|
+
SupportsKeysAndGetItemOrIterable = t.Union[
|
|
33
|
+
SupportsKeysAndGetItem, t.Iterable[t.Tuple[Any, Any]]
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
25
37
|
class MultiDict(MutableMapping):
|
|
26
38
|
"""
|
|
27
39
|
Dictionary-like object that supports multiple values per key. Insertion
|
|
@@ -44,9 +56,13 @@ class MultiDict(MutableMapping):
|
|
|
44
56
|
|
|
45
57
|
"""
|
|
46
58
|
|
|
59
|
+
_dict: Dict[Any, Any]
|
|
60
|
+
_order: t.List[Any]
|
|
47
61
|
setdefault = MutableMapping.setdefault
|
|
48
62
|
|
|
49
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self, mapping_or_iterable: SupportsKeysAndGetItemOrIterable = tuple(), **kwargs
|
|
65
|
+
):
|
|
50
66
|
"""
|
|
51
67
|
MultiDicts can be constructed in the following ways:
|
|
52
68
|
|
|
@@ -76,13 +92,9 @@ class MultiDict(MutableMapping):
|
|
|
76
92
|
MultiDict(...)
|
|
77
93
|
|
|
78
94
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
% (self.__class__.__name__, 1 + len(args))
|
|
83
|
-
)
|
|
84
|
-
self.clear()
|
|
85
|
-
self.update(*args, **kwargs)
|
|
95
|
+
self._order = []
|
|
96
|
+
self._dict = {}
|
|
97
|
+
self._update(mapping_or_iterable, True, kwargs)
|
|
86
98
|
|
|
87
99
|
def __getitem__(self, key):
|
|
88
100
|
"""
|
|
@@ -289,7 +301,7 @@ class MultiDict(MutableMapping):
|
|
|
289
301
|
self._order.remove((key, value))
|
|
290
302
|
return value
|
|
291
303
|
|
|
292
|
-
def popitem(self):
|
|
304
|
+
def popitem(self) -> t.Tuple[Any, Any]:
|
|
293
305
|
"""
|
|
294
306
|
Return and remove a ``(key, value)`` pair from the dictionary.
|
|
295
307
|
|
|
@@ -314,7 +326,17 @@ class MultiDict(MutableMapping):
|
|
|
314
326
|
raise KeyError("popitem(): dictionary is empty")
|
|
315
327
|
return key, self.pop(key)
|
|
316
328
|
|
|
317
|
-
|
|
329
|
+
@t.overload
|
|
330
|
+
def update(
|
|
331
|
+
self, mapping_or_iterable: SupportsKeysAndGetItem, /, **kwargs: Any
|
|
332
|
+
) -> None:
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
@t.overload
|
|
336
|
+
def update(self, /, **kwargs: Any) -> None:
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
def update(self, mapping_or_iterable=tuple(), **kwargs):
|
|
318
340
|
r"""
|
|
319
341
|
Update the MultiDict from another MultiDict, regular dictionary or a
|
|
320
342
|
iterable of ``(key, value)`` pairs. New keys overwrite old keys - use
|
|
@@ -349,29 +371,24 @@ class MultiDict(MutableMapping):
|
|
|
349
371
|
>>> d.update(mood='okay')
|
|
350
372
|
|
|
351
373
|
"""
|
|
352
|
-
|
|
353
|
-
raise TypeError("expected at most 1 argument, got %d" % (1 + len(args),))
|
|
354
|
-
if args:
|
|
355
|
-
other = args[0]
|
|
356
|
-
else:
|
|
357
|
-
other = []
|
|
358
|
-
return self._update(other, True, **kwargs)
|
|
374
|
+
self._update(mapping_or_iterable, True, kwargs)
|
|
359
375
|
|
|
360
|
-
def extend(
|
|
376
|
+
def extend(
|
|
377
|
+
self, mapping_or_iterable: SupportsKeysAndGetItemOrIterable = tuple(), **kwargs
|
|
378
|
+
):
|
|
361
379
|
"""
|
|
362
380
|
Extend the MultiDict with another MultiDict, regular dictionary or a
|
|
363
381
|
iterable of ``(key, value)`` pairs. This is similar to :meth:`update`
|
|
364
382
|
except that new keys are added to old keys.
|
|
365
383
|
"""
|
|
366
|
-
|
|
367
|
-
raise TypeError("expected at most 1 argument, got %d", (1 + len(args),))
|
|
368
|
-
if args:
|
|
369
|
-
other = args[0]
|
|
370
|
-
else:
|
|
371
|
-
other = []
|
|
372
|
-
return self._update(other, False, **kwargs)
|
|
384
|
+
return self._update(mapping_or_iterable, False, kwargs)
|
|
373
385
|
|
|
374
|
-
def _update(
|
|
386
|
+
def _update(
|
|
387
|
+
self,
|
|
388
|
+
other: SupportsKeysAndGetItemOrIterable,
|
|
389
|
+
replace: bool,
|
|
390
|
+
extra_kwargs: Dict[Any, Any],
|
|
391
|
+
):
|
|
375
392
|
"""
|
|
376
393
|
Update the MultiDict from another object and optionally kwargs.
|
|
377
394
|
|
|
@@ -380,17 +397,15 @@ class MultiDict(MutableMapping):
|
|
|
380
397
|
:param replace: if ``True``, entries will replace rather than extend
|
|
381
398
|
existing entries (second positional arg)
|
|
382
399
|
"""
|
|
383
|
-
other, replace = args
|
|
384
|
-
|
|
385
400
|
if isinstance(other, self.__class__):
|
|
386
401
|
items = list(other.allitems())
|
|
387
|
-
elif isinstance(other,
|
|
402
|
+
elif isinstance(other, Mapping):
|
|
388
403
|
items = list(other.items())
|
|
389
404
|
else:
|
|
390
405
|
items = list(other)
|
|
391
406
|
|
|
392
|
-
if
|
|
393
|
-
items += list(
|
|
407
|
+
if extra_kwargs:
|
|
408
|
+
items += list(extra_kwargs.items())
|
|
394
409
|
|
|
395
410
|
if replace:
|
|
396
411
|
replaced = {k for k, v in items if k in self._dict}
|
|
@@ -398,9 +413,11 @@ class MultiDict(MutableMapping):
|
|
|
398
413
|
for key in replaced:
|
|
399
414
|
self._dict[key] = []
|
|
400
415
|
|
|
416
|
+
setdefault = self._dict.setdefault
|
|
417
|
+
append_order = self._order.append
|
|
401
418
|
for k, v in items:
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
setdefault(k, []).append(v)
|
|
420
|
+
append_order((k, v))
|
|
404
421
|
|
|
405
422
|
def __repr__(self):
|
|
406
423
|
"""
|
|
@@ -428,4 +445,4 @@ class MultiDict(MutableMapping):
|
|
|
428
445
|
|
|
429
446
|
def clear(self):
|
|
430
447
|
self._order = []
|
|
431
|
-
self._dict
|
|
448
|
+
self._dict = {}
|
fresco/options.py
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import inspect
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
import typing as t
|
|
18
19
|
import os
|
|
19
20
|
import re
|
|
20
21
|
from decimal import Decimal
|
|
@@ -31,6 +32,8 @@ from typing import Union
|
|
|
31
32
|
|
|
32
33
|
from fresco.exceptions import OptionsLoadedException
|
|
33
34
|
|
|
35
|
+
__all__ = ["Options"]
|
|
36
|
+
|
|
34
37
|
logger = logging.getLogger(__name__)
|
|
35
38
|
|
|
36
39
|
|
|
@@ -72,25 +75,39 @@ class Options(dict):
|
|
|
72
75
|
for func in self._loaded_callbacks:
|
|
73
76
|
func(self)
|
|
74
77
|
|
|
78
|
+
def trigger_onload(self):
|
|
79
|
+
"""
|
|
80
|
+
Mark the options object as having loaded and call any registered
|
|
81
|
+
onload callbacks
|
|
82
|
+
"""
|
|
83
|
+
if self._is_loaded:
|
|
84
|
+
raise OptionsLoadedException("Options have already been loaded")
|
|
85
|
+
self.do_loaded_callbacks()
|
|
86
|
+
self.__dict__["_is_loaded"] = True
|
|
87
|
+
|
|
75
88
|
def copy(self):
|
|
76
89
|
return self.__class__(super().copy())
|
|
77
90
|
|
|
78
91
|
def load(
|
|
79
92
|
self,
|
|
80
|
-
sources: str,
|
|
93
|
+
sources: t.Union[str, t.Iterable[t.Union[Path, str]]],
|
|
81
94
|
tags: Sequence[str] = [],
|
|
82
95
|
use_environ=False,
|
|
83
96
|
strict=True,
|
|
84
97
|
dir=None,
|
|
98
|
+
trigger_onload=True,
|
|
85
99
|
):
|
|
86
100
|
"""
|
|
87
101
|
Find all files matching glob pattern ``sources`` and populates the
|
|
88
102
|
options object from those with matching filenames containing ``tags``.
|
|
89
103
|
|
|
90
|
-
:param sources:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
:param sources:
|
|
105
|
+
one or more glob patterns separated by ";",
|
|
106
|
+
or a list of glob patterns
|
|
107
|
+
|
|
108
|
+
:param tags:
|
|
109
|
+
a list of tags to look for in file names.
|
|
110
|
+
|
|
94
111
|
:param use_environ: if true, environment variables matching previously
|
|
95
112
|
loaded keys will be loaded into the options object.
|
|
96
113
|
This happens after all files have been processed.
|
|
@@ -102,13 +119,21 @@ class Options(dict):
|
|
|
102
119
|
format. Any other files will be interpreted as simple lists of
|
|
103
120
|
```key=value`` pairs.
|
|
104
121
|
|
|
105
|
-
Tags in filenames
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
Tags in filenames are delimited with periods,
|
|
123
|
+
for example ".env.production.py".
|
|
124
|
+
The filename ``setttings.dev.local.ini`` would be
|
|
125
|
+
considered to have the tags ``('dev', 'local')``
|
|
126
|
+
|
|
127
|
+
Where filename contain multiple tags, all tags must match for the file
|
|
128
|
+
to be loaded.
|
|
129
|
+
|
|
130
|
+
Tag names may contain the names of environment variable surrounded by
|
|
131
|
+
braces, for example ``{USER}``. These will be substituted for the
|
|
132
|
+
environment variable's value, with any dots or path separators replaced
|
|
133
|
+
by underscores.
|
|
108
134
|
|
|
109
|
-
The
|
|
110
|
-
|
|
111
|
-
underscores.
|
|
135
|
+
The special variable ``{hostname}`` will be substituted for the current
|
|
136
|
+
host's name with dots replaced by underscores.
|
|
112
137
|
|
|
113
138
|
Files with the suffix ".sample" are unconditionally excluded.
|
|
114
139
|
|
|
@@ -118,7 +143,7 @@ class Options(dict):
|
|
|
118
143
|
Example::
|
|
119
144
|
|
|
120
145
|
opts = Options()
|
|
121
|
-
opts.load(
|
|
146
|
+
opts.load(".env*", ["dev", "host-{hostname}", "local"])
|
|
122
147
|
|
|
123
148
|
Would load options from files named ``.env``, ``.env.json``, ``.env.dev.py``
|
|
124
149
|
and ``.env.local.py.``
|
|
@@ -126,15 +151,24 @@ class Options(dict):
|
|
|
126
151
|
"""
|
|
127
152
|
if self._is_loaded:
|
|
128
153
|
raise OptionsLoadedException("Options have already been loaded")
|
|
129
|
-
|
|
154
|
+
|
|
155
|
+
tag_substitutions = os.environ.copy()
|
|
156
|
+
tag_substitutions["hostname"] = gethostname()
|
|
157
|
+
tag_substitutions = {
|
|
158
|
+
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
159
|
+
for k, v in tag_substitutions.items()
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
candidates: List[Path] = []
|
|
131
163
|
if dir is None:
|
|
132
164
|
dir = Path(".")
|
|
133
165
|
else:
|
|
134
166
|
dir = Path(dir)
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
if isinstance(sources, str):
|
|
169
|
+
sources = [s.strip() for s in sources.split(";")]
|
|
170
|
+
for source in sources:
|
|
171
|
+
sourcepath = dir / Path(source)
|
|
138
172
|
candidates.extend(
|
|
139
173
|
p
|
|
140
174
|
for p in sourcepath.parent.glob(sourcepath.name)
|
|
@@ -142,7 +176,13 @@ class Options(dict):
|
|
|
142
176
|
)
|
|
143
177
|
|
|
144
178
|
if tags:
|
|
145
|
-
|
|
179
|
+
subbed_tags = []
|
|
180
|
+
for tag in tags:
|
|
181
|
+
try:
|
|
182
|
+
subbed_tags.append(tag.format(**tag_substitutions))
|
|
183
|
+
except KeyError:
|
|
184
|
+
pass
|
|
185
|
+
tags = subbed_tags
|
|
146
186
|
tagged_sources = []
|
|
147
187
|
for p in candidates:
|
|
148
188
|
candidate_tags = [t for t in str(p.name).split(".") if t][1:]
|
|
@@ -216,8 +256,9 @@ class Options(dict):
|
|
|
216
256
|
if k in os.environ:
|
|
217
257
|
self[k] = parse_value(self, os.environ[k])
|
|
218
258
|
|
|
219
|
-
|
|
220
|
-
|
|
259
|
+
if trigger_onload:
|
|
260
|
+
self.do_loaded_callbacks()
|
|
261
|
+
self.__dict__["_is_loaded"] = True
|
|
221
262
|
return self
|
|
222
263
|
|
|
223
264
|
def update_from_file(self, path, load_all=False):
|
|
@@ -256,9 +297,11 @@ class Options(dict):
|
|
|
256
297
|
|
|
257
298
|
def update_from_object(self, ob, load_all=False):
|
|
258
299
|
"""
|
|
259
|
-
Update the instance with any symbols
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
Update the instance with any symbols found in object ``ob``.
|
|
301
|
+
|
|
302
|
+
:param load_all:
|
|
303
|
+
If true private symbols will also be loaded into the options
|
|
304
|
+
object.
|
|
262
305
|
"""
|
|
263
306
|
self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
|
|
264
307
|
|
|
@@ -298,3 +341,70 @@ def parse_key_value_pairs(options, lines: Iterable[str]):
|
|
|
298
341
|
k = k.strip()
|
|
299
342
|
values[k] = options[k] = parse_value(options, v)
|
|
300
343
|
return values
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def list_from_str(s, separator=","):
|
|
347
|
+
"""
|
|
348
|
+
Return the value of ``s`` split into a list of times by ``separator``.
|
|
349
|
+
Spaces around items will be stripped.
|
|
350
|
+
"""
|
|
351
|
+
if isinstance(s, str):
|
|
352
|
+
if not s.strip():
|
|
353
|
+
return []
|
|
354
|
+
return [item.strip() for item in s.split(",")]
|
|
355
|
+
raise TypeError(f"Expected string, got {s!r}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def dict_from_options(
|
|
359
|
+
prefix: str, options: t.Mapping[str, Any], recursive: bool = False
|
|
360
|
+
) -> t.Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Create a dictionary containing all items in ``options`` whose keys start
|
|
363
|
+
with ``prefix``, with that prefix stripped.
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
|
|
367
|
+
>>> from fresco.options import Options, dict_from_options
|
|
368
|
+
>>> options = Options(
|
|
369
|
+
... {"DATABASE_HOST": "127.0.0.1", "DATABASE_USER": "scott"}
|
|
370
|
+
... )
|
|
371
|
+
>>> dict_from_options("DATABASE_", options)
|
|
372
|
+
{'HOST': '127.0.0.1', 'USER': 'scott'}
|
|
373
|
+
|
|
374
|
+
If ``recursive`` is True, a recursive splitting will be
|
|
375
|
+
applied. The last character of ``prefix`` will be used as the separator,
|
|
376
|
+
for example::
|
|
377
|
+
|
|
378
|
+
>>> from fresco.options import dict_from_options
|
|
379
|
+
>>> options = Options(
|
|
380
|
+
... {
|
|
381
|
+
... "DATABASE_DEV_HOST": "127.0.0.1",
|
|
382
|
+
... "DATABASE_DEV_USER": "scott",
|
|
383
|
+
... "DATABASE_PROD_HOST": "192.168.0.78",
|
|
384
|
+
... "DATABASE_PROD_HOST": "tiger",
|
|
385
|
+
... }
|
|
386
|
+
... )
|
|
387
|
+
>>> dict_from_options("DATABASE_", options, recursive=True)
|
|
388
|
+
{'DEV': {'HOST': '127.0.0.1', ...}, 'PROD': {'HOST', ...}}
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
prefixlen = len(prefix)
|
|
393
|
+
d = {k[prefixlen:]: v for k, v in options.items() if k.startswith(prefix)}
|
|
394
|
+
|
|
395
|
+
def recursive_split(d: t.Mapping[str, Any], sep: str) -> t.Dict[str, Any]:
|
|
396
|
+
d_: t.Dict[str, Any] = {}
|
|
397
|
+
for k in d:
|
|
398
|
+
if sep in k:
|
|
399
|
+
ks = k.split(sep)
|
|
400
|
+
subdict = d_
|
|
401
|
+
for subkey in ks[:-1]:
|
|
402
|
+
subdict = subdict.setdefault(subkey, {})
|
|
403
|
+
subdict[ks[-1]] = d[k]
|
|
404
|
+
else:
|
|
405
|
+
d_[k] = d[k]
|
|
406
|
+
return d_
|
|
407
|
+
|
|
408
|
+
if recursive:
|
|
409
|
+
return recursive_split(d, prefix[-1])
|
|
410
|
+
return d
|
fresco/request.py
CHANGED
|
@@ -137,7 +137,7 @@ class Request(object):
|
|
|
137
137
|
the ``query`` property.
|
|
138
138
|
"""
|
|
139
139
|
if self._form is None:
|
|
140
|
-
if self.environ["REQUEST_METHOD"] in
|
|
140
|
+
if self.environ["REQUEST_METHOD"] in {"PUT", "POST"}:
|
|
141
141
|
items, close = parse_post(
|
|
142
142
|
self.environ,
|
|
143
143
|
self.environ["wsgi.input"],
|
fresco/requestcontext.py
CHANGED
|
@@ -16,13 +16,16 @@ from collections import defaultdict
|
|
|
16
16
|
from threading import get_ident
|
|
17
17
|
from typing import Any
|
|
18
18
|
from typing import Dict
|
|
19
|
-
|
|
19
|
+
import typing as t
|
|
20
20
|
|
|
21
|
-
__all__ = "context", "RequestContext"
|
|
21
|
+
__all__ = ["context", "RequestContext"]
|
|
22
|
+
|
|
23
|
+
ContextDict = t.Dict[str, Any]
|
|
24
|
+
ContextStack = t.List[ContextDict]
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class RequestContext(object):
|
|
25
|
-
"""
|
|
28
|
+
"""
|
|
26
29
|
A local storage class that maintains different values for each request
|
|
27
30
|
context in which it is used.
|
|
28
31
|
|
|
@@ -42,13 +45,17 @@ class RequestContext(object):
|
|
|
42
45
|
|
|
43
46
|
__slots__ = ["_contexts", "_ident_func"]
|
|
44
47
|
|
|
45
|
-
def __init__(
|
|
46
|
-
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
ident_func: t.Callable[[], t.Any] = get_ident,
|
|
51
|
+
setattr=object.__setattr__
|
|
52
|
+
):
|
|
53
|
+
c: Dict[Any, ContextStack] = defaultdict(list)
|
|
47
54
|
c[ident_func()] = [{}]
|
|
48
55
|
setattr(self, "_contexts", c)
|
|
49
56
|
setattr(self, "_ident_func", ident_func)
|
|
50
57
|
|
|
51
|
-
def push(self, **bindnames):
|
|
58
|
+
def push(self, **bindnames: t.Any):
|
|
52
59
|
self._contexts[self._ident_func()].append(bindnames)
|
|
53
60
|
|
|
54
61
|
def pop(self):
|
|
@@ -58,7 +65,7 @@ class RequestContext(object):
|
|
|
58
65
|
if not ctx:
|
|
59
66
|
del self._contexts[ident]
|
|
60
67
|
|
|
61
|
-
def currentcontext(self):
|
|
68
|
+
def currentcontext(self) -> ContextDict:
|
|
62
69
|
return self._contexts[self._ident_func()][-1]
|
|
63
70
|
|
|
64
71
|
def __getattr__(self, item):
|
|
@@ -67,7 +74,7 @@ class RequestContext(object):
|
|
|
67
74
|
except (KeyError, IndexError):
|
|
68
75
|
raise AttributeError(item)
|
|
69
76
|
|
|
70
|
-
def __setattr__(self, item, ob):
|
|
77
|
+
def __setattr__(self, item: str, ob: t.Any):
|
|
71
78
|
self._contexts[self._ident_func()][-1][item] = ob
|
|
72
79
|
|
|
73
80
|
def __delattr__(self, item):
|
|
@@ -81,7 +88,7 @@ class RequestContext(object):
|
|
|
81
88
|
__setitem__ = __setattr__
|
|
82
89
|
__delitem__ = __delattr__
|
|
83
90
|
|
|
84
|
-
def get(self, item, default=None):
|
|
91
|
+
def get(self, item: str, default=None):
|
|
85
92
|
return self.currentcontext().get(item, default)
|
|
86
93
|
|
|
87
94
|
def __repr__(self):
|
fresco/response.py
CHANGED
|
@@ -261,7 +261,7 @@ default_charset = "UTF-8"
|
|
|
261
261
|
|
|
262
262
|
|
|
263
263
|
def encoder(stream, charset):
|
|
264
|
-
|
|
264
|
+
"""
|
|
265
265
|
Encode a response iterator using the given character set.
|
|
266
266
|
"""
|
|
267
267
|
if charset is None:
|
|
@@ -326,7 +326,7 @@ def make_headers(
|
|
|
326
326
|
|
|
327
327
|
|
|
328
328
|
class Response(object):
|
|
329
|
-
"""
|
|
329
|
+
"""
|
|
330
330
|
Model an HTTP response
|
|
331
331
|
"""
|
|
332
332
|
|
|
@@ -500,7 +500,7 @@ class Response(object):
|
|
|
500
500
|
)
|
|
501
501
|
|
|
502
502
|
def get_headers(self, name):
|
|
503
|
-
"""
|
|
503
|
+
"""
|
|
504
504
|
Return the list of headers set with the given name.
|
|
505
505
|
|
|
506
506
|
Synopsis::
|
|
@@ -515,7 +515,7 @@ class Response(object):
|
|
|
515
515
|
]
|
|
516
516
|
|
|
517
517
|
def get_header(self, name, default=""):
|
|
518
|
-
"""
|
|
518
|
+
"""
|
|
519
519
|
Return the concatenated values of the named header(s) or ``default`` if
|
|
520
520
|
the header has not been set.
|
|
521
521
|
|
|
@@ -547,7 +547,7 @@ class Response(object):
|
|
|
547
547
|
|
|
548
548
|
@property
|
|
549
549
|
def content_type(self):
|
|
550
|
-
"""
|
|
550
|
+
"""
|
|
551
551
|
Return the value of the ``Content-Type`` header if set, otherwise
|
|
552
552
|
``None``.
|
|
553
553
|
"""
|
|
@@ -557,7 +557,7 @@ class Response(object):
|
|
|
557
557
|
return None
|
|
558
558
|
|
|
559
559
|
def add_header(self, name, value, make_header_name=make_header_name):
|
|
560
|
-
"""
|
|
560
|
+
"""
|
|
561
561
|
Return a new response object with the given additional header.
|
|
562
562
|
|
|
563
563
|
Synopsis::
|
|
@@ -571,7 +571,7 @@ class Response(object):
|
|
|
571
571
|
return self.replace(headers=(self.headers + [(make_header_name(name), value)]))
|
|
572
572
|
|
|
573
573
|
def add_headers(self, headers=[], **kwheaders):
|
|
574
|
-
"""
|
|
574
|
+
"""
|
|
575
575
|
Return a new response object with the given additional headers.
|
|
576
576
|
|
|
577
577
|
Synopsis::
|
|
@@ -587,7 +587,7 @@ class Response(object):
|
|
|
587
587
|
return self.replace(headers=make_headers(self.headers + headers, kwheaders))
|
|
588
588
|
|
|
589
589
|
def remove_headers(self, *headers):
|
|
590
|
-
"""
|
|
590
|
+
"""
|
|
591
591
|
Return a new response object with the named headers removed.
|
|
592
592
|
|
|
593
593
|
Synopsis::
|
|
@@ -617,7 +617,7 @@ class Response(object):
|
|
|
617
617
|
httponly=False,
|
|
618
618
|
samesite="Lax",
|
|
619
619
|
):
|
|
620
|
-
"""
|
|
620
|
+
"""
|
|
621
621
|
Return a new response object with the given cookie added.
|
|
622
622
|
|
|
623
623
|
Synopsis::
|
|
@@ -720,7 +720,7 @@ class Response(object):
|
|
|
720
720
|
)
|
|
721
721
|
|
|
722
722
|
def buffered(self):
|
|
723
|
-
"""
|
|
723
|
+
"""
|
|
724
724
|
Return a new response object with the content buffered into a list.
|
|
725
725
|
This will also generate a content-length header.
|
|
726
726
|
|
|
@@ -756,7 +756,7 @@ class Response(object):
|
|
|
756
756
|
|
|
757
757
|
@classmethod
|
|
758
758
|
def not_found(cls, request=None):
|
|
759
|
-
"""
|
|
759
|
+
"""
|
|
760
760
|
Return an HTTP not found response (404).
|
|
761
761
|
|
|
762
762
|
Synopsis::
|
|
@@ -824,7 +824,7 @@ class Response(object):
|
|
|
824
824
|
|
|
825
825
|
@classmethod
|
|
826
826
|
def forbidden(cls, message="Sorry, access is denied"):
|
|
827
|
-
"""
|
|
827
|
+
"""
|
|
828
828
|
Return an HTTP forbidden response (403).
|
|
829
829
|
|
|
830
830
|
Synopsis::
|
|
@@ -840,7 +840,7 @@ class Response(object):
|
|
|
840
840
|
|
|
841
841
|
@classmethod
|
|
842
842
|
def bad_request(cls, request=None):
|
|
843
|
-
"""
|
|
843
|
+
"""
|
|
844
844
|
Return an HTTP bad request response.
|
|
845
845
|
|
|
846
846
|
Synopsis::
|
|
@@ -863,7 +863,7 @@ class Response(object):
|
|
|
863
863
|
|
|
864
864
|
@classmethod
|
|
865
865
|
def length_required(cls, request=None):
|
|
866
|
-
"""
|
|
866
|
+
"""
|
|
867
867
|
Return an HTTP Length Required response (411).
|
|
868
868
|
|
|
869
869
|
Synopsis::
|
|
@@ -886,7 +886,7 @@ class Response(object):
|
|
|
886
886
|
|
|
887
887
|
@classmethod
|
|
888
888
|
def payload_too_large(cls, request=None):
|
|
889
|
-
"""
|
|
889
|
+
"""
|
|
890
890
|
Return an HTTP Payload Too Large response (413)::
|
|
891
891
|
|
|
892
892
|
>>> response = Response.payload_too_large()
|
|
@@ -901,7 +901,7 @@ class Response(object):
|
|
|
901
901
|
|
|
902
902
|
@classmethod
|
|
903
903
|
def method_not_allowed(cls, valid_methods):
|
|
904
|
-
"""
|
|
904
|
+
"""
|
|
905
905
|
Return an HTTP method not allowed response (405)::
|
|
906
906
|
|
|
907
907
|
>>> from fresco import context
|
|
@@ -923,7 +923,7 @@ class Response(object):
|
|
|
923
923
|
|
|
924
924
|
@classmethod
|
|
925
925
|
def internal_server_error(cls):
|
|
926
|
-
"""
|
|
926
|
+
"""
|
|
927
927
|
Return an HTTP internal server error response (500).
|
|
928
928
|
|
|
929
929
|
Synopsis::
|
|
@@ -944,7 +944,7 @@ class Response(object):
|
|
|
944
944
|
def unrestricted_redirect(
|
|
945
945
|
cls, location, request=None, status=STATUS_FOUND, **kwargs
|
|
946
946
|
):
|
|
947
|
-
"""
|
|
947
|
+
"""
|
|
948
948
|
Return an HTTP redirect reponse (30x).
|
|
949
949
|
|
|
950
950
|
:param location: The redirect location or a view specification.
|
|
@@ -1060,7 +1060,8 @@ class Response(object):
|
|
|
1060
1060
|
:param kwargs: kwargs to be passed to :func:`fresco.core.urlfor` to
|
|
1061
1061
|
construct the redirect URL, or the fallback in the case
|
|
1062
1062
|
that ``location`` is already a qualified URL.
|
|
1063
|
-
|
|
1063
|
+
|
|
1064
|
+
Synopsis::
|
|
1064
1065
|
|
|
1065
1066
|
>>> def view():
|
|
1066
1067
|
... return Response.redirect("/new-location")
|
|
@@ -1084,7 +1085,7 @@ class Response(object):
|
|
|
1084
1085
|
|
|
1085
1086
|
@classmethod
|
|
1086
1087
|
def redirect_permanent(cls, *args, **kwargs):
|
|
1087
|
-
"""
|
|
1088
|
+
"""
|
|
1088
1089
|
Return an HTTP permanent redirect reponse.
|
|
1089
1090
|
|
|
1090
1091
|
:param location: the URI of the new location. If relative this will be
|
|
@@ -1097,7 +1098,7 @@ class Response(object):
|
|
|
1097
1098
|
|
|
1098
1099
|
@classmethod
|
|
1099
1100
|
def redirect_temporary(cls, *args, **kwargs):
|
|
1100
|
-
"""
|
|
1101
|
+
"""
|
|
1101
1102
|
Return an HTTP permanent redirect reponse.
|
|
1102
1103
|
|
|
1103
1104
|
:param location: the URI of the new location. If relative this will be
|
|
@@ -1110,7 +1111,7 @@ class Response(object):
|
|
|
1110
1111
|
|
|
1111
1112
|
@classmethod
|
|
1112
1113
|
def meta_refresh(cls, location, delay=1, request=None):
|
|
1113
|
-
"""
|
|
1114
|
+
"""
|
|
1114
1115
|
Return an HTML page containing a <meta http-equiv="refresh"> tag,
|
|
1115
1116
|
causing the browser to redirect to the given location after ``delay``
|
|
1116
1117
|
seconds.
|
|
@@ -1172,7 +1173,7 @@ class Response(object):
|
|
|
1172
1173
|
|
|
1173
1174
|
|
|
1174
1175
|
def dump_response(r, line_break=b"\r\n", encoding="UTF-8"):
|
|
1175
|
-
"""
|
|
1176
|
+
"""
|
|
1176
1177
|
Return a byte-string representation of the given response, as for a HTTP
|
|
1177
1178
|
response.
|
|
1178
1179
|
"""
|
fresco/routing.py
CHANGED
|
@@ -370,15 +370,15 @@ class PathConverter(StrConverter):
|
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
class MatchAllURLsPattern(Pattern):
|
|
373
|
-
"""
|
|
374
|
-
A pattern matcher that matches all
|
|
375
|
-
arguments are parsed from the URL.
|
|
373
|
+
"""
|
|
374
|
+
A pattern matcher that matches all paths starting with the given prefix.
|
|
375
|
+
No arguments are parsed from the URL.
|
|
376
376
|
"""
|
|
377
377
|
|
|
378
378
|
def __init__(self, path):
|
|
379
379
|
self.path = path
|
|
380
380
|
|
|
381
|
-
def match(self, path):
|
|
381
|
+
def match(self, path: str) -> t.Optional[PathMatch]:
|
|
382
382
|
if path.startswith(self.path):
|
|
383
383
|
return PathMatch(self.path, path[len(self.path) :], (), {})
|
|
384
384
|
return None
|
|
@@ -400,6 +400,32 @@ class MatchAllURLsPattern(Pattern):
|
|
|
400
400
|
return "%s*" % (self.path,)
|
|
401
401
|
|
|
402
402
|
|
|
403
|
+
class PrefixPattern(MatchAllURLsPattern):
|
|
404
|
+
"""
|
|
405
|
+
A pattern matcher that matches the given prefix. The prefix will only be
|
|
406
|
+
matched at a path separator boundary.
|
|
407
|
+
|
|
408
|
+
``PrefixPattern('/foo')`` will match the paths ``/foo`` and ``/foo/bar``,
|
|
409
|
+
but not ``/foobar``.
|
|
410
|
+
|
|
411
|
+
No arguments are parsed from the URL.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
def __init__(self, path):
|
|
415
|
+
super().__init__(path)
|
|
416
|
+
if path[-1] == "/":
|
|
417
|
+
self.path_with_sep = path
|
|
418
|
+
else:
|
|
419
|
+
self.path_with_sep = f"{self.path}/"
|
|
420
|
+
|
|
421
|
+
def match(self, path: str) -> t.Optional[PathMatch]:
|
|
422
|
+
prefix = path[: len(self.path_with_sep)]
|
|
423
|
+
|
|
424
|
+
if prefix == self.path or prefix == self.path_with_sep:
|
|
425
|
+
return PathMatch(self.path, path[len(self.path) :], (), {})
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
|
|
403
429
|
class ExtensiblePattern(Pattern):
|
|
404
430
|
"""\
|
|
405
431
|
An extensible URL pattern matcher.
|
|
@@ -547,10 +573,10 @@ class ExtensiblePattern(Pattern):
|
|
|
547
573
|
"""
|
|
548
574
|
return eval("(lambda *args, **kwargs: (args, kwargs))(%s)" % argstr)
|
|
549
575
|
|
|
550
|
-
def match(self, path):
|
|
576
|
+
def match(self, path) -> t.Optional[PathMatch]:
|
|
551
577
|
"""
|
|
552
|
-
Test ``path`` and return a
|
|
553
|
-
|
|
578
|
+
Test ``path`` and return a PathMatch object or ``None`` if there was no
|
|
579
|
+
match.
|
|
554
580
|
"""
|
|
555
581
|
mo = self.regex_match(path)
|
|
556
582
|
if mo is None:
|
|
@@ -1358,13 +1384,18 @@ class RouteCollection(MutableSequence):
|
|
|
1358
1384
|
exc = self.__routed_views__[viewspec] = RouteNotFound(viewspec)
|
|
1359
1385
|
raise exc
|
|
1360
1386
|
|
|
1361
|
-
def _get_routes(
|
|
1387
|
+
def _get_routes(
|
|
1388
|
+
self, key: Tuple[t.Optional[str], str]
|
|
1389
|
+
) -> t.Sequence[t.Tuple[Route, t.Optional[PathMatch]]]:
|
|
1362
1390
|
method, path = key
|
|
1363
1391
|
routes = ((r, r.match(path, method)) for r in self.__routes__)
|
|
1364
1392
|
return [(r, t) for (r, t) in routes if t is not None]
|
|
1365
1393
|
|
|
1366
1394
|
def get_route_traversals(
|
|
1367
|
-
self,
|
|
1395
|
+
self,
|
|
1396
|
+
path: str,
|
|
1397
|
+
method: Optional[str],
|
|
1398
|
+
request: Optional[Request] = None,
|
|
1368
1399
|
) -> t.Iterator[RouteTraversal]:
|
|
1369
1400
|
"""
|
|
1370
1401
|
Generate RouteTraversals for routes matching the given path and
|
|
@@ -1604,7 +1635,7 @@ class RouteCollection(MutableSequence):
|
|
|
1604
1635
|
:param path: the path prefix at which the view will be routed
|
|
1605
1636
|
"""
|
|
1606
1637
|
return self.route(
|
|
1607
|
-
|
|
1638
|
+
PrefixPattern(path),
|
|
1608
1639
|
methods,
|
|
1609
1640
|
view,
|
|
1610
1641
|
route_class=route_class,
|
fresco/subrequests.py
CHANGED
|
@@ -34,12 +34,12 @@ def subrequest(view, *args, **kwargs):
|
|
|
34
34
|
- **A callable**: a new subrequest context will be created and
|
|
35
35
|
``view(*args, **kwargs)`` will be called. Middleware and application
|
|
36
36
|
hooks will not be called, and any
|
|
37
|
-
:class:`~fresco.routeargs.RouteArg`
|
|
37
|
+
:class:`~fresco.routeargs.RouteArg` instances will not be resolved.
|
|
38
38
|
|
|
39
39
|
- **Any other value**: the view function is looked up using the same
|
|
40
40
|
route resolution rules as :meth:`~fresco.core.FrescoApp.urlfor`.
|
|
41
41
|
Middleware and hooks will be skipped, but
|
|
42
|
-
:class:`~fresco.routeargs.RouteArg`
|
|
42
|
+
:class:`~fresco.routeargs.RouteArg` instances will be resolved.
|
|
43
43
|
|
|
44
44
|
If you pass in a view callable you can force RouteArgs to be resolved by
|
|
45
45
|
specifying ``_resolve=True``.
|
|
@@ -272,6 +272,7 @@ def _get_args_for_route(
|
|
|
272
272
|
route_kwargs = {}
|
|
273
273
|
route_args = list(
|
|
274
274
|
chain(
|
|
275
|
+
[request] if route.provide_request else [],
|
|
275
276
|
((a(request) if isinstance(a, RouteArg) else a) for a in route.view_args),
|
|
276
277
|
(
|
|
277
278
|
mutable_args.pop(0)
|
fresco/util/http.py
CHANGED
|
@@ -202,6 +202,7 @@ def parse_querystring(
|
|
|
202
202
|
charset: Optional[str] = None,
|
|
203
203
|
strict: bool = False,
|
|
204
204
|
keep_blank_values: bool = True,
|
|
205
|
+
unquote_plus=unquote_plus,
|
|
205
206
|
) -> List[Tuple[str, str]]:
|
|
206
207
|
"""
|
|
207
208
|
Return ``(key, value)`` pairs from the given querystring::
|
|
@@ -519,7 +520,7 @@ def read_until(
|
|
|
519
520
|
exhausted.
|
|
520
521
|
"""
|
|
521
522
|
buf = b""
|
|
522
|
-
found = None
|
|
523
|
+
found: t.Optional[bool] = None
|
|
523
524
|
|
|
524
525
|
def _found():
|
|
525
526
|
if found is None:
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
fresco/__init__.py,sha256=
|
|
1
|
+
fresco/__init__.py,sha256=e0KXsDXDex3v2Efu5cGbMvN-jttFfz1aDo7pyG7O_rE,3520
|
|
2
2
|
fresco/cookie.py,sha256=Qnx8yOjU4LUJ1fqi7YvqbhAA01rCsclJGl_fxI68slw,7055
|
|
3
|
-
fresco/core.py,sha256=
|
|
3
|
+
fresco/core.py,sha256=mwYR6UP0zRSjcFlNZf51-otX9EcmvgjEZ7GDVQoQexg,26615
|
|
4
4
|
fresco/decorators.py,sha256=84NUpRJ-M7GK6wDl42bmCRSvgoWzCsy1huyvGnSAPPw,3232
|
|
5
5
|
fresco/exceptions.py,sha256=KE-LoYUGnho6KltzkU6cnm9vUiUhAiDIjPqn5ba-YCA,4410
|
|
6
|
-
fresco/middleware.py,sha256=
|
|
7
|
-
fresco/multidict.py,sha256=
|
|
8
|
-
fresco/options.py,sha256=
|
|
9
|
-
fresco/request.py,sha256=
|
|
10
|
-
fresco/requestcontext.py,sha256=
|
|
11
|
-
fresco/response.py,sha256=
|
|
6
|
+
fresco/middleware.py,sha256=uCAuOtBnvaVBJGrQa8ecvLkSZ6aSKmWaJqxB8Rv4SsQ,4173
|
|
7
|
+
fresco/multidict.py,sha256=0CaNNIcFnZ1hLk3NExhNvjc_BtK4zVB26L9gP_7MeNM,13362
|
|
8
|
+
fresco/options.py,sha256=JneUzO1Ep4c3UbX-1icBZ9cZGheVmZAXdvegrbI39mw,13614
|
|
9
|
+
fresco/request.py,sha256=JCETzqzGZgPpS7IBdqi4Hk3CTwTGiaTtRXFW_nPP1yg,27071
|
|
10
|
+
fresco/requestcontext.py,sha256=P-SkKJkKLYVqNiR2zwooRROnSnE2VMj2P2-eD5DW5Qg,3504
|
|
11
|
+
fresco/response.py,sha256=Pz8icriWhHNVsTkB5x5y99yvcz-nFC7Eobuv7tfLE3s,37078
|
|
12
12
|
fresco/routeargs.py,sha256=dxNlqbQ1FrgIY6OCFzcEMdZ0OVyjlMQdQGLmUJgdPYU,10176
|
|
13
|
-
fresco/routing.py,sha256=
|
|
13
|
+
fresco/routing.py,sha256=nh7TiyDPQ1Y4RCXJGj_Wk-BP1UqpNyh6BKqxhNHlQ8U,58670
|
|
14
14
|
fresco/static.py,sha256=9SKQ3P1YFTP45Qiic-ycvkpKRvqIURp1XSzPazTmYLI,2513
|
|
15
|
-
fresco/subrequests.py,sha256=
|
|
15
|
+
fresco/subrequests.py,sha256=wFJWLuhVAcei5imYc5NBSCBaHBEm-X4_XswPtK3O4Zw,11081
|
|
16
16
|
fresco/types.py,sha256=UHITRMDoS90s2zV2wNpqLFhRWESfaBAMuQnL4c21mqY,106
|
|
17
17
|
fresco/typing.py,sha256=jQ1r_wme54J08XZUdmuuW4M8ve-gEhm1Wriwctls3r8,347
|
|
18
18
|
fresco/util/__init__.py,sha256=mJkaZzvYgBnxsBAGv8y_P1yzonHqWgw6VF2Zs4rmJEA,7
|
|
@@ -20,15 +20,15 @@ fresco/util/cache.py,sha256=EjzF9EzzDw4U4coOykkJEgh_8HMDpwhEbYKBo_TeOZQ,1609
|
|
|
20
20
|
fresco/util/common.py,sha256=8lvrjhELvYsUWxu7DZi1OJcUOFk2ILYndNsnaS0IqjM,1258
|
|
21
21
|
fresco/util/contentencodings.py,sha256=kRFQze8pi6o8bh1nMJa2BZRTAZ5Ud6PPBIhofhhZNDQ,5484
|
|
22
22
|
fresco/util/file.py,sha256=Vp7qJTo9RouUeHq25ExyBGkGTHuW-9Q7D_0GB54DFe8,1383
|
|
23
|
-
fresco/util/http.py,sha256=
|
|
23
|
+
fresco/util/http.py,sha256=GQC-wL0lUpOF8zogrFHrh_3D7UbrksrLg_OVsv1c74Y,22175
|
|
24
24
|
fresco/util/io.py,sha256=xxwDNJOcewY8lAR4Ce3cmB_zlrys8JGsESgwGWE198Y,1289
|
|
25
25
|
fresco/util/object.py,sha256=FjYNfPHzvBqq1rn0Y6As-2AVZ_SZOjH-lrSy4EbYmHY,370
|
|
26
26
|
fresco/util/security.py,sha256=nXEdoCak_2c4OA1L1wGwhZygS22s2fzwR0Kp-DdwKZg,1058
|
|
27
27
|
fresco/util/textproc.py,sha256=e5jLTofKCqdm6_Fy8XOyR43AJr5APtL59Kd8cNA9PrQ,2309
|
|
28
28
|
fresco/util/urls.py,sha256=aaVovLyXNlVoGviiLN94ImqXf-LTQs_xooEIyi3LBc4,9195
|
|
29
29
|
fresco/util/wsgi.py,sha256=dV69VjJ7W8OqAxWzlA0dz7vAuC1MliNBnbr1MSyB6Is,12970
|
|
30
|
-
fresco-3.
|
|
31
|
-
fresco-3.
|
|
32
|
-
fresco-3.
|
|
33
|
-
fresco-3.
|
|
34
|
-
fresco-3.
|
|
30
|
+
fresco-3.3.1.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
31
|
+
fresco-3.3.1.dist-info/METADATA,sha256=DEjrYiyD-gUvPOYEjyZz8y1y8PBrzIUxPx9wFuv7w_o,1575
|
|
32
|
+
fresco-3.3.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
33
|
+
fresco-3.3.1.dist-info/top_level.txt,sha256=p_1aMce5Shjq9fIfdbB-aN8wCDhjF_iYnn98bUebbII,7
|
|
34
|
+
fresco-3.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|