fresco 3.3.4__py3-none-any.whl → 3.9.0__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 +55 -56
- fresco/core.py +39 -27
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +188 -70
- fresco/py.typed +0 -0
- fresco/request.py +157 -36
- fresco/requestcontext.py +3 -0
- fresco/response.py +66 -57
- fresco/routeargs.py +23 -9
- fresco/routing.py +152 -74
- fresco/static.py +1 -1
- fresco/subrequests.py +3 -5
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +59 -15
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_response.py +16 -0
- fresco/tests/test_routing.py +113 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/tests/util/test_urls.py +20 -0
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +44 -12
- fresco/util/wsgi.py +15 -14
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/METADATA +4 -4
- fresco-3.9.0.dist-info/RECORD +58 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.3.4.dist-info/RECORD +0 -57
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/top_level.txt +0 -0
fresco/response.py
CHANGED
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
The :class:`Response` class models the response from your application to a
|
|
17
17
|
single request.
|
|
18
18
|
"""
|
|
19
|
+
from collections.abc import Iterable
|
|
19
20
|
from datetime import datetime
|
|
20
21
|
from itertools import chain
|
|
21
22
|
from typing import Callable
|
|
22
23
|
from typing import List
|
|
23
24
|
from typing import Tuple
|
|
24
25
|
from typing import Set
|
|
26
|
+
import typing as t
|
|
25
27
|
import re
|
|
26
28
|
import json as stdlib_json
|
|
27
29
|
|
|
@@ -275,7 +277,7 @@ def encoder(stream, charset):
|
|
|
275
277
|
|
|
276
278
|
|
|
277
279
|
def make_header_name(name):
|
|
278
|
-
"""
|
|
280
|
+
"""
|
|
279
281
|
Return a formatted header name from a python idenfier.
|
|
280
282
|
|
|
281
283
|
Example usage::
|
|
@@ -331,6 +333,7 @@ class Response(object):
|
|
|
331
333
|
"""
|
|
332
334
|
|
|
333
335
|
default_content_type = "text/html; charset=UTF-8"
|
|
336
|
+
content_iterator: Iterable[bytes]
|
|
334
337
|
|
|
335
338
|
def __init__(
|
|
336
339
|
self,
|
|
@@ -386,7 +389,7 @@ class Response(object):
|
|
|
386
389
|
self.status = "200 OK"
|
|
387
390
|
else:
|
|
388
391
|
try:
|
|
389
|
-
self.status = f"{status} {HTTP_STATUS_CODES[status]}"
|
|
392
|
+
self.status = f"{status} {HTTP_STATUS_CODES[status]}" # type: ignore
|
|
390
393
|
except KeyError:
|
|
391
394
|
self.status = str(status)
|
|
392
395
|
|
|
@@ -441,7 +444,7 @@ class Response(object):
|
|
|
441
444
|
else:
|
|
442
445
|
self.content_iterator = encoder(content, self.charset)
|
|
443
446
|
|
|
444
|
-
def __call__(self, environ, start_response, exc_info=None):
|
|
447
|
+
def __call__(self, environ, start_response, exc_info=None) -> Iterable[bytes]:
|
|
445
448
|
"""
|
|
446
449
|
WSGI callable. Calls ``start_response`` with assigned headers and
|
|
447
450
|
returns an iterator over ``content``.
|
|
@@ -449,7 +452,7 @@ class Response(object):
|
|
|
449
452
|
start_response(self.status, self.headers, exc_info)
|
|
450
453
|
result = self.content_iterator
|
|
451
454
|
if self.onclose:
|
|
452
|
-
result = ClosingIterator(result, *self.onclose)
|
|
455
|
+
result = ClosingIterator[bytes](result, *self.onclose)
|
|
453
456
|
return result
|
|
454
457
|
|
|
455
458
|
def add_onclose(self, *funcs):
|
|
@@ -610,12 +613,12 @@ class Response(object):
|
|
|
610
613
|
value,
|
|
611
614
|
max_age=None,
|
|
612
615
|
expires=None,
|
|
613
|
-
path="/",
|
|
614
|
-
secure=
|
|
615
|
-
domain=None,
|
|
616
|
-
comment=None,
|
|
617
|
-
httponly=False,
|
|
618
|
-
samesite="Lax",
|
|
616
|
+
path: str = "/",
|
|
617
|
+
secure: bool = False,
|
|
618
|
+
domain: t.Optional[str] = None,
|
|
619
|
+
comment: t.Optional[str] = None,
|
|
620
|
+
httponly: bool = False,
|
|
621
|
+
samesite: str = "Lax",
|
|
619
622
|
):
|
|
620
623
|
"""
|
|
621
624
|
Return a new response object with the given cookie added.
|
|
@@ -674,8 +677,10 @@ class Response(object):
|
|
|
674
677
|
newheaders.append((k, v))
|
|
675
678
|
return self.replace(headers=newheaders + [("Vary", ", ".join(_vary_on))])
|
|
676
679
|
|
|
677
|
-
def replace(
|
|
678
|
-
|
|
680
|
+
def replace(
|
|
681
|
+
self, content=None, status=None, headers=None, **kwheaders
|
|
682
|
+
) -> "Response":
|
|
683
|
+
"""
|
|
679
684
|
Return a new response object with any of content, status or headers
|
|
680
685
|
changed.
|
|
681
686
|
|
|
@@ -755,7 +760,7 @@ class Response(object):
|
|
|
755
760
|
return default_charset
|
|
756
761
|
|
|
757
762
|
@classmethod
|
|
758
|
-
def not_found(cls, request=None):
|
|
763
|
+
def not_found(cls, request=None) -> "Response":
|
|
759
764
|
"""
|
|
760
765
|
Return an HTTP not found response (404).
|
|
761
766
|
|
|
@@ -778,7 +783,7 @@ class Response(object):
|
|
|
778
783
|
)
|
|
779
784
|
|
|
780
785
|
@classmethod
|
|
781
|
-
def error(cls, message="500 Internal Server Error"):
|
|
786
|
+
def error(cls, message="500 Internal Server Error") -> "Response":
|
|
782
787
|
"""
|
|
783
788
|
Return an HTTP server error response (500).
|
|
784
789
|
|
|
@@ -800,7 +805,7 @@ class Response(object):
|
|
|
800
805
|
)
|
|
801
806
|
|
|
802
807
|
@classmethod
|
|
803
|
-
def unauthorized(cls, authenticate):
|
|
808
|
+
def unauthorized(cls, authenticate) -> "Response":
|
|
804
809
|
"""
|
|
805
810
|
Return an HTTP unauthorized response (401)
|
|
806
811
|
"""
|
|
@@ -815,7 +820,7 @@ class Response(object):
|
|
|
815
820
|
)
|
|
816
821
|
|
|
817
822
|
@classmethod
|
|
818
|
-
def unauthorized_basic(cls, realm):
|
|
823
|
+
def unauthorized_basic(cls, realm) -> "Response":
|
|
819
824
|
"""
|
|
820
825
|
Return an HTTP unauthorized response (401) with a WWW-Authenticate
|
|
821
826
|
header set for HTTP Basic authentication..
|
|
@@ -823,7 +828,7 @@ class Response(object):
|
|
|
823
828
|
return cls.unauthorized(authenticate=f'Basic realm="{realm}"')
|
|
824
829
|
|
|
825
830
|
@classmethod
|
|
826
|
-
def forbidden(cls, message="Sorry, access is denied"):
|
|
831
|
+
def forbidden(cls, message="Sorry, access is denied") -> "Response":
|
|
827
832
|
"""
|
|
828
833
|
Return an HTTP forbidden response (403).
|
|
829
834
|
|
|
@@ -839,7 +844,9 @@ class Response(object):
|
|
|
839
844
|
)
|
|
840
845
|
|
|
841
846
|
@classmethod
|
|
842
|
-
def bad_request(
|
|
847
|
+
def bad_request(
|
|
848
|
+
cls, message="The server could not understand your request", request=None
|
|
849
|
+
) -> "Response":
|
|
843
850
|
"""
|
|
844
851
|
Return an HTTP bad request response.
|
|
845
852
|
|
|
@@ -853,16 +860,17 @@ class Response(object):
|
|
|
853
860
|
return cls(
|
|
854
861
|
status=STATUS_BAD_REQUEST,
|
|
855
862
|
content=[
|
|
856
|
-
"<html>"
|
|
857
|
-
"<body>"
|
|
858
|
-
"<h1>
|
|
859
|
-
"</
|
|
860
|
-
"</
|
|
863
|
+
f"<html>"
|
|
864
|
+
f"<body>"
|
|
865
|
+
f"<h1>Bad request</h1>"
|
|
866
|
+
f"<p>{message}</p>"
|
|
867
|
+
f"</body>"
|
|
868
|
+
f"</html>"
|
|
861
869
|
],
|
|
862
870
|
)
|
|
863
871
|
|
|
864
872
|
@classmethod
|
|
865
|
-
def length_required(cls, request=None):
|
|
873
|
+
def length_required(cls, request=None) -> "Response":
|
|
866
874
|
"""
|
|
867
875
|
Return an HTTP Length Required response (411).
|
|
868
876
|
|
|
@@ -885,7 +893,7 @@ class Response(object):
|
|
|
885
893
|
)
|
|
886
894
|
|
|
887
895
|
@classmethod
|
|
888
|
-
def payload_too_large(cls, request=None):
|
|
896
|
+
def payload_too_large(cls, request=None) -> "Response":
|
|
889
897
|
"""
|
|
890
898
|
Return an HTTP Payload Too Large response (413)::
|
|
891
899
|
|
|
@@ -900,7 +908,7 @@ class Response(object):
|
|
|
900
908
|
request_entity_too_large = payload_too_large
|
|
901
909
|
|
|
902
910
|
@classmethod
|
|
903
|
-
def method_not_allowed(cls, valid_methods):
|
|
911
|
+
def method_not_allowed(cls, valid_methods) -> "Response":
|
|
904
912
|
"""
|
|
905
913
|
Return an HTTP method not allowed response (405)::
|
|
906
914
|
|
|
@@ -922,7 +930,7 @@ class Response(object):
|
|
|
922
930
|
)
|
|
923
931
|
|
|
924
932
|
@classmethod
|
|
925
|
-
def internal_server_error(cls):
|
|
933
|
+
def internal_server_error(cls) -> "Response":
|
|
926
934
|
"""
|
|
927
935
|
Return an HTTP internal server error response (500).
|
|
928
936
|
|
|
@@ -943,7 +951,7 @@ class Response(object):
|
|
|
943
951
|
@classmethod
|
|
944
952
|
def unrestricted_redirect(
|
|
945
953
|
cls, location, request=None, status=STATUS_FOUND, **kwargs
|
|
946
|
-
):
|
|
954
|
+
) -> "Response":
|
|
947
955
|
"""
|
|
948
956
|
Return an HTTP redirect reponse (30x).
|
|
949
957
|
|
|
@@ -1001,7 +1009,7 @@ class Response(object):
|
|
|
1001
1009
|
request = fresco.context.request
|
|
1002
1010
|
location = request.resolve_url(location)
|
|
1003
1011
|
|
|
1004
|
-
return
|
|
1012
|
+
return cls(
|
|
1005
1013
|
"<html><head></head><body>\n"
|
|
1006
1014
|
"<h1>Page has moved</h1>\n"
|
|
1007
1015
|
"<p><a href='%s'>%s</a></p>\n"
|
|
@@ -1011,8 +1019,8 @@ class Response(object):
|
|
|
1011
1019
|
)
|
|
1012
1020
|
|
|
1013
1021
|
@classmethod
|
|
1014
|
-
def unrestricted_redirect_permanent(cls, *args, **kwargs):
|
|
1015
|
-
"""
|
|
1022
|
+
def unrestricted_redirect_permanent(cls, *args, **kwargs) -> "Response":
|
|
1023
|
+
"""
|
|
1016
1024
|
Return an HTTP permanent redirect reponse.
|
|
1017
1025
|
|
|
1018
1026
|
:param location: the URI of the new location. If relative this will be
|
|
@@ -1024,8 +1032,8 @@ class Response(object):
|
|
|
1024
1032
|
return cls.unrestricted_redirect(*args, **kwargs)
|
|
1025
1033
|
|
|
1026
1034
|
@classmethod
|
|
1027
|
-
def unrestricted_redirect_temporary(cls, *args, **kwargs):
|
|
1028
|
-
"""
|
|
1035
|
+
def unrestricted_redirect_temporary(cls, *args, **kwargs) -> "Response":
|
|
1036
|
+
"""
|
|
1029
1037
|
Return an HTTP permanent redirect reponse.
|
|
1030
1038
|
|
|
1031
1039
|
:param location: the URI of the new location. If relative this will be
|
|
@@ -1045,7 +1053,7 @@ class Response(object):
|
|
|
1045
1053
|
_is_safe_url=is_safe_url,
|
|
1046
1054
|
allowed_hosts=frozenset(),
|
|
1047
1055
|
**kwargs,
|
|
1048
|
-
):
|
|
1056
|
+
) -> "Response":
|
|
1049
1057
|
"""
|
|
1050
1058
|
Return an HTTP redirect reponse (30x). Will only redirect to the
|
|
1051
1059
|
current host or hosts in the ``allowed_hosts`` list. A ``ValueError``
|
|
@@ -1072,19 +1080,22 @@ class Response(object):
|
|
|
1072
1080
|
"""
|
|
1073
1081
|
# Create the fallback redirect response always so that
|
|
1074
1082
|
# changes to route names don't result in latent bugs
|
|
1083
|
+
fallback_response = None
|
|
1075
1084
|
if fallback:
|
|
1076
|
-
|
|
1077
|
-
|
|
1085
|
+
fallback_response = cls.unrestricted_redirect(
|
|
1086
|
+
fallback, status=status, **kwargs
|
|
1087
|
+
)
|
|
1088
|
+
if url and (
|
|
1089
|
+
callable(url) or "//" not in url or _is_safe_url(url, allowed_hosts)
|
|
1090
|
+
):
|
|
1078
1091
|
return cls.unrestricted_redirect(url, status=status, **kwargs)
|
|
1079
1092
|
|
|
1080
|
-
if
|
|
1081
|
-
return
|
|
1082
|
-
if fallback:
|
|
1083
|
-
return fallback
|
|
1093
|
+
if fallback_response:
|
|
1094
|
+
return fallback_response
|
|
1084
1095
|
raise ValueError("Unsafe URL")
|
|
1085
1096
|
|
|
1086
1097
|
@classmethod
|
|
1087
|
-
def redirect_permanent(cls, *args, **kwargs):
|
|
1098
|
+
def redirect_permanent(cls, *args, **kwargs) -> "Response":
|
|
1088
1099
|
"""
|
|
1089
1100
|
Return an HTTP permanent redirect reponse.
|
|
1090
1101
|
|
|
@@ -1097,7 +1108,7 @@ class Response(object):
|
|
|
1097
1108
|
return cls.redirect(*args, **kwargs)
|
|
1098
1109
|
|
|
1099
1110
|
@classmethod
|
|
1100
|
-
def redirect_temporary(cls, *args, **kwargs):
|
|
1111
|
+
def redirect_temporary(cls, *args, **kwargs) -> "Response":
|
|
1101
1112
|
"""
|
|
1102
1113
|
Return an HTTP permanent redirect reponse.
|
|
1103
1114
|
|
|
@@ -1110,7 +1121,7 @@ class Response(object):
|
|
|
1110
1121
|
return cls.redirect(*args, **kwargs)
|
|
1111
1122
|
|
|
1112
1123
|
@classmethod
|
|
1113
|
-
def meta_refresh(cls, location, delay=1,
|
|
1124
|
+
def meta_refresh(cls, location, delay=1, **kwargs) -> "Response":
|
|
1114
1125
|
"""
|
|
1115
1126
|
Return an HTML page containing a <meta http-equiv="refresh"> tag,
|
|
1116
1127
|
causing the browser to redirect to the given location after ``delay``
|
|
@@ -1121,21 +1132,19 @@ class Response(object):
|
|
|
1121
1132
|
request.
|
|
1122
1133
|
|
|
1123
1134
|
"""
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
'<body><p><a href="{0}">Click here to continue</a></p></body>'
|
|
1135
|
-
"</html>"
|
|
1136
|
-
).format(location)
|
|
1135
|
+
response = cls.redirect(location, **kwargs)
|
|
1136
|
+
location = response.get_header("location")
|
|
1137
|
+
return response.replace(
|
|
1138
|
+
status="200 OK",
|
|
1139
|
+
content=[
|
|
1140
|
+
f"<!DOCTYPE html>"
|
|
1141
|
+
f"<html>"
|
|
1142
|
+
f'<head><meta http-equiv="refresh" content="0; url={location}"></head>'
|
|
1143
|
+
f'<body><p><a href="{location}">Click here to continue</a></p></body>'
|
|
1144
|
+
f"</html>"
|
|
1137
1145
|
],
|
|
1138
1146
|
content_type="text/html",
|
|
1147
|
+
location=None,
|
|
1139
1148
|
)
|
|
1140
1149
|
|
|
1141
1150
|
@classmethod
|
|
@@ -1149,7 +1158,7 @@ class Response(object):
|
|
|
1149
1158
|
headers=None,
|
|
1150
1159
|
dumps=stdlib_json.dumps,
|
|
1151
1160
|
**kwargs,
|
|
1152
|
-
):
|
|
1161
|
+
) -> "Response":
|
|
1153
1162
|
"""
|
|
1154
1163
|
Return an ``application/json`` response with the given data
|
|
1155
1164
|
JSON serialized
|
fresco/routeargs.py
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
#
|
|
15
|
+
from collections.abc import Sequence
|
|
15
16
|
from operator import attrgetter, itemgetter, methodcaller
|
|
16
17
|
from fresco.exceptions import MissingRouteArg
|
|
17
18
|
from fresco.request import Request
|
|
@@ -19,6 +20,7 @@ from itertools import cycle
|
|
|
19
20
|
from typing import Any
|
|
20
21
|
from typing import Callable
|
|
21
22
|
from typing import Mapping
|
|
23
|
+
from typing import Union
|
|
22
24
|
|
|
23
25
|
__all__ = (
|
|
24
26
|
"routearg",
|
|
@@ -138,7 +140,7 @@ def map_magics(dest):
|
|
|
138
140
|
|
|
139
141
|
|
|
140
142
|
@map_magics("raise_exception")
|
|
141
|
-
class LazyException
|
|
143
|
+
class LazyException:
|
|
142
144
|
"""
|
|
143
145
|
A lazy exception uses magic methods to intercept any access to the object
|
|
144
146
|
and raise an exception.
|
|
@@ -151,7 +153,7 @@ class LazyException(object):
|
|
|
151
153
|
raise self.__dict__["_exception"]
|
|
152
154
|
|
|
153
155
|
|
|
154
|
-
class RouteArg
|
|
156
|
+
class RouteArg:
|
|
155
157
|
"""
|
|
156
158
|
RouteArg objects can be used as keyword arguments in a route definition.
|
|
157
159
|
RouteArgs can extract information from the request and make it available
|
|
@@ -194,7 +196,7 @@ class RouteArg(object):
|
|
|
194
196
|
self.route = route
|
|
195
197
|
self.name = name
|
|
196
198
|
|
|
197
|
-
def __call__(self, request):
|
|
199
|
+
def __call__(self, request: Request) -> Any:
|
|
198
200
|
return None
|
|
199
201
|
|
|
200
202
|
|
|
@@ -225,7 +227,7 @@ def routearg(func, *args, **kwargs):
|
|
|
225
227
|
|
|
226
228
|
|
|
227
229
|
class RequestArg(RouteArg):
|
|
228
|
-
"""
|
|
230
|
+
"""
|
|
229
231
|
Extract a view keyword argument from the request object.
|
|
230
232
|
"""
|
|
231
233
|
|
|
@@ -241,9 +243,19 @@ class RequestArg(RouteArg):
|
|
|
241
243
|
#: input
|
|
242
244
|
converter_exceptions = (ValueError, TypeError)
|
|
243
245
|
|
|
246
|
+
converter: Union[
|
|
247
|
+
Callable[[list[str]], Any],
|
|
248
|
+
Callable[[str], Any],
|
|
249
|
+
]
|
|
250
|
+
|
|
244
251
|
def __init__(
|
|
245
252
|
self,
|
|
246
|
-
converter
|
|
253
|
+
converter: Union[
|
|
254
|
+
Sequence[
|
|
255
|
+
Callable[[str], Any],
|
|
256
|
+
],
|
|
257
|
+
Callable[[str], Any]
|
|
258
|
+
] = str,
|
|
247
259
|
key=None,
|
|
248
260
|
default=_marker,
|
|
249
261
|
exception=MissingRouteArg,
|
|
@@ -251,11 +263,13 @@ class RequestArg(RouteArg):
|
|
|
251
263
|
super(RequestArg, self).__init__(default, exception)
|
|
252
264
|
self.formkey = key
|
|
253
265
|
self.getter: Callable[[Mapping], Any]
|
|
254
|
-
self.is_list = isinstance(converter,
|
|
266
|
+
self.is_list = isinstance(converter, Sequence)
|
|
255
267
|
if self.is_list:
|
|
256
|
-
|
|
268
|
+
def list_converter(vs: list[str]) -> list[Any]:
|
|
269
|
+
return [c(v) for c, v in zip(cycle(converter), vs)] # type: ignore
|
|
270
|
+
self.converter = list_converter
|
|
257
271
|
else:
|
|
258
|
-
self.converter = converter
|
|
272
|
+
self.converter = converter # type: ignore
|
|
259
273
|
|
|
260
274
|
def configure(self, route, name):
|
|
261
275
|
if self.formkey is None:
|
|
@@ -372,7 +386,7 @@ class CookieArg(RequestArg):
|
|
|
372
386
|
|
|
373
387
|
source = RequestArg.cookies
|
|
374
388
|
|
|
375
|
-
def getter(self, cookies):
|
|
389
|
+
def getter(self, cookies): # type: ignore
|
|
376
390
|
if self.is_list:
|
|
377
391
|
return cookies.getlist(self.formkey)
|
|
378
392
|
else:
|