fresco 3.2.0__tar.gz → 3.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {fresco-3.2.0 → fresco-3.9.0}/CHANGELOG.rst +73 -0
  2. {fresco-3.2.0/fresco.egg-info → fresco-3.9.0}/PKG-INFO +8 -9
  3. fresco-3.9.0/fresco/__init__.py +127 -0
  4. {fresco-3.2.0 → fresco-3.9.0}/fresco/core.py +49 -35
  5. {fresco-3.2.0 → fresco-3.9.0}/fresco/decorators.py +6 -3
  6. fresco-3.9.0/fresco/defaults.py +1 -0
  7. {fresco-3.2.0 → fresco-3.9.0}/fresco/middleware.py +46 -25
  8. {fresco-3.2.0 → fresco-3.9.0}/fresco/multidict.py +66 -65
  9. fresco-3.9.0/fresco/options.py +536 -0
  10. {fresco-3.2.0 → fresco-3.9.0}/fresco/request.py +159 -38
  11. {fresco-3.2.0 → fresco-3.9.0}/fresco/requestcontext.py +19 -9
  12. {fresco-3.2.0 → fresco-3.9.0}/fresco/response.py +90 -80
  13. {fresco-3.2.0 → fresco-3.9.0}/fresco/routeargs.py +23 -9
  14. {fresco-3.2.0 → fresco-3.9.0}/fresco/routing.py +192 -82
  15. {fresco-3.2.0 → fresco-3.9.0}/fresco/static.py +1 -1
  16. {fresco-3.2.0 → fresco-3.9.0}/fresco/subrequests.py +6 -7
  17. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_core.py +4 -4
  18. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_multidict.py +2 -2
  19. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_options.py +137 -8
  20. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_request.py +21 -10
  21. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_response.py +16 -0
  22. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_routing.py +142 -30
  23. fresco-3.9.0/fresco/tests/util/__init__.py +0 -0
  24. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/test_http.py +1 -3
  25. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/test_urls.py +20 -0
  26. fresco-3.9.0/fresco/types.py +32 -0
  27. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/cache.py +2 -1
  28. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/contentencodings.py +2 -1
  29. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/http.py +69 -46
  30. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/urls.py +44 -12
  31. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/wsgi.py +16 -15
  32. {fresco-3.2.0 → fresco-3.9.0/fresco.egg-info}/PKG-INFO +8 -9
  33. {fresco-3.2.0 → fresco-3.9.0}/fresco.egg-info/SOURCES.txt +3 -3
  34. fresco-3.9.0/pyproject.toml +47 -0
  35. fresco-3.9.0/setup.cfg +4 -0
  36. fresco-3.2.0/fresco/__init__.py +0 -27
  37. fresco-3.2.0/fresco/options.py +0 -300
  38. fresco-3.2.0/fresco/types.py +0 -6
  39. fresco-3.2.0/fresco/typing.py +0 -11
  40. fresco-3.2.0/setup.cfg +0 -31
  41. fresco-3.2.0/setup.py +0 -17
  42. {fresco-3.2.0 → fresco-3.9.0}/LICENSE.txt +0 -0
  43. {fresco-3.2.0 → fresco-3.9.0}/MANIFEST.in +0 -0
  44. {fresco-3.2.0 → fresco-3.9.0}/README.rst +0 -0
  45. {fresco-3.2.0 → fresco-3.9.0}/fresco/cookie.py +0 -0
  46. {fresco-3.2.0 → fresco-3.9.0}/fresco/exceptions.py +0 -0
  47. /fresco-3.2.0/fresco/tests/__init__.py → /fresco-3.9.0/fresco/py.typed +0 -0
  48. {fresco-3.2.0/fresco/tests/util → fresco-3.9.0/fresco/tests}/__init__.py +0 -0
  49. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/fixtures.py +0 -0
  50. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_cookie.py +0 -0
  51. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_decorators.py +0 -0
  52. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_exceptions.py +0 -0
  53. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_middleware.py +0 -0
  54. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_requestcontext.py +0 -0
  55. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_routeargs.py +0 -0
  56. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_static.py +0 -0
  57. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/test_subrequests.py +0 -0
  58. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/form_data.py +0 -0
  59. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/test_common.py +0 -0
  60. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/test_security.py +0 -0
  61. {fresco-3.2.0 → fresco-3.9.0}/fresco/tests/util/test_wsgi.py +0 -0
  62. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/__init__.py +0 -0
  63. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/common.py +0 -0
  64. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/file.py +0 -0
  65. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/io.py +0 -0
  66. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/object.py +0 -0
  67. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/security.py +0 -0
  68. {fresco-3.2.0 → fresco-3.9.0}/fresco/util/textproc.py +0 -0
  69. {fresco-3.2.0 → fresco-3.9.0}/fresco.egg-info/dependency_links.txt +0 -0
  70. {fresco-3.2.0 → fresco-3.9.0}/fresco.egg-info/top_level.txt +0 -0
@@ -1,6 +1,79 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 3.9.0 (released 2026-02-02)
5
+ ---------------------------
6
+
7
+ - Bugfix: fix request.get and request.getint type annotations
8
+ - Allow multiple methods to be routed using kwargs, eg ``Route(GET_POST=my_view)``
9
+ - Add support for Python 3.13, 3.14 and drop support for Python 3.9
10
+
11
+ 3.8.0 (released 2025-07-17)
12
+ ---------------------------
13
+
14
+ - Add Response.bad_request ``message`` argument
15
+ - Add warnings when loading option files creates undefined keys
16
+
17
+ 3.7.0 (released 2025-05-05)
18
+ ---------------------------
19
+
20
+ - Add a ``fresco.util.url.add_query`` function
21
+
22
+ 3.6.0 (released 2025-05-03)
23
+ ---------------------------
24
+
25
+ - Replace ``fresco.DEFAULT_CHARSET`` with ``fresco.defaults.DEFAULT_CHARSET``
26
+ - Add a ``required`` argument to Request.get
27
+ - Add new request.get* functions: ``getfloat``, ``getbool`` and ``getdecimal``
28
+
29
+ 3.5.0 (released 2025-02-25)
30
+ ---------------------------
31
+
32
+ - Refactor middleware.XForwarded to clean up error tracebacks and add a new ``force_https`` option
33
+ - Add new ``fresco.options.override_options`` function to aid testing
34
+ - Automatically add a ``request`` argument where the routed function's first
35
+ positional parameter is annotated as type ``fresco.request.Request``.
36
+ view
37
+
38
+ 3.4.0 (released 2024-10-16)
39
+ ---------------------------
40
+
41
+ - Add new ``ALL`` shortcut method, allowing all HTTP methods to be routed to a
42
+ view
43
+
44
+ 3.3.4 (released 2024-08-06)
45
+ ---------------------------
46
+
47
+ - Bugfix: ensure option files are loaded in a well defined order
48
+
49
+ 3.3.3 (released 2024-05-01)
50
+ ---------------------------
51
+
52
+ - Bugfix: request.now returns the correct time on non-UTC systems
53
+ - Add support for Python 3.12 and drop support for 3.8
54
+
55
+ 3.3.2 (released 2023-05-31)
56
+ ---------------------------
57
+
58
+ - Bugfix: a fresco app routed through route_wsgi now receives a fresh request
59
+ and environ dict, isolated from the containing app.
60
+
61
+ 3.3.1 (released 2023-05-18)
62
+ ---------------------------
63
+
64
+ - The ``route_wsgi`` and ``route_all`` methods now only match at a
65
+ path separator boundary (``'/'``).
66
+ - Bugfix: subrequest now works with views routed via ``RRoute`` (and so expect
67
+ an initial ``request`` argument)
68
+
69
+ 3.3.0 (released 2023-05-02)
70
+ ----------------------------
71
+
72
+ - Options: fresco.options.Options can now take a list of paths as its first argument
73
+ - Options: tags can now be specified with environment variable substitutions
74
+ (eg "{FOO}" would load files tagged with the current value of the 'FOO'
75
+ environment variable)
76
+ - Options: add ``fresco.options.list_from_str`` and ``fresco.options.dict_from_options``
4
77
 
5
78
  3.2.0 (released 2023-04-16)
6
79
  ---------------------------
@@ -1,22 +1,21 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: fresco
3
- Version: 3.2.0
3
+ Version: 3.9.0
4
4
  Summary: A Web/WSGI micro-framework
5
- Home-page: https://ollycope.com/software/fresco/latest/
6
- Author: Oliver Cope
7
- Author-email: oliver@redgecko.org
8
- License: Apache
9
- Keywords: wsgi web www framework
5
+ Author-email: Oliver Cope <oliver@redgecko.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://ollycope.com/software/fresco/latest/
8
+ Keywords: wsgi,web,www,framework
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: Apache Software License
14
12
  Classifier: Operating System :: OS Independent
15
13
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: Implementation :: PyPy
17
14
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
18
15
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
16
+ Description-Content-Type: text/x-rst
19
17
  License-File: LICENSE.txt
18
+ Dynamic: license-file
20
19
 
21
20
  Fresco, a web micro-framework for Python
22
21
  ========================================
@@ -0,0 +1,127 @@
1
+ # Copyright 2015 Oliver Cope
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ from fresco.request import Request
16
+ from fresco.request import currentrequest
17
+ from fresco.requestcontext import context
18
+ from fresco.response import Response
19
+ from fresco.core import FrescoApp
20
+ from fresco.core import urlfor
21
+ from fresco.defaults import DEFAULT_CHARSET
22
+ from fresco.options import Options
23
+ from fresco.routing import Route
24
+ from fresco.routing import RRoute
25
+ from fresco.routing import RouteCollection
26
+ from fresco.routing import DelegateRoute
27
+ from fresco.routing import routefor
28
+ from fresco.routing import ALL_METHODS
29
+ from fresco.routing import GET
30
+ from fresco.routing import HEAD
31
+ from fresco.routing import POST
32
+ from fresco.routing import PUT
33
+ from fresco.routing import DELETE
34
+ from fresco.routing import OPTIONS
35
+ from fresco.routing import TRACE
36
+ from fresco.routing import CONNECT
37
+ from fresco.routing import VERSION_CONTROL
38
+ from fresco.routing import REPORT
39
+ from fresco.routing import CHECKOUT
40
+ from fresco.routing import CHECKIN
41
+ from fresco.routing import UNCHECKOUT
42
+ from fresco.routing import MKWORKSPACE
43
+ from fresco.routing import UPDATE
44
+ from fresco.routing import LABEL
45
+ from fresco.routing import MERGE
46
+ from fresco.routing import BASELINE_CONTROL
47
+ from fresco.routing import MKACTIVITY
48
+ from fresco.routing import ORDERPATCH
49
+ from fresco.routing import ACL
50
+ from fresco.routing import SEARCH
51
+ from fresco.routing import PATCH
52
+ from fresco.routeargs import routearg
53
+ from fresco.routeargs import FormArg
54
+ from fresco.routeargs import PostArg
55
+ from fresco.routeargs import QueryArg
56
+ from fresco.routeargs import GetArg
57
+ from fresco.routeargs import CookieArg
58
+ from fresco.routeargs import SessionArg
59
+ from fresco.routeargs import RequestObject
60
+ from fresco.routeargs import FormData
61
+ from fresco.routeargs import PostData
62
+ from fresco.routeargs import QueryData
63
+ from fresco.routeargs import GetData
64
+ from fresco.middleware import XForwarded
65
+ from fresco.subrequests import subrequest
66
+ from fresco.subrequests import subrequest_bytes
67
+ from fresco.subrequests import subrequest_raw
68
+ from fresco.util.common import object_or_404
69
+
70
+
71
+ __version__ = "3.9.0"
72
+ __all__ = [
73
+ "Request",
74
+ "currentrequest",
75
+ "context",
76
+ "Response",
77
+ "FrescoApp",
78
+ "urlfor",
79
+ "ALL_METHODS",
80
+ "DEFAULT_CHARSET",
81
+ "GET",
82
+ "HEAD",
83
+ "POST",
84
+ "PUT",
85
+ "DELETE",
86
+ "OPTIONS",
87
+ "TRACE",
88
+ "CONNECT",
89
+ "VERSION_CONTROL",
90
+ "REPORT",
91
+ "CHECKOUT",
92
+ "CHECKIN",
93
+ "UNCHECKOUT",
94
+ "MKWORKSPACE",
95
+ "UPDATE",
96
+ "LABEL",
97
+ "MERGE",
98
+ "BASELINE_CONTROL",
99
+ "MKACTIVITY",
100
+ "ORDERPATCH",
101
+ "ACL",
102
+ "SEARCH",
103
+ "PATCH",
104
+ "DelegateRoute",
105
+ "Options",
106
+ "Route",
107
+ "RouteCollection",
108
+ "RRoute",
109
+ "routearg",
110
+ "FormArg",
111
+ "PostArg",
112
+ "QueryArg",
113
+ "GetArg",
114
+ "CookieArg",
115
+ "SessionArg",
116
+ "RequestObject",
117
+ "FormData",
118
+ "PostData",
119
+ "QueryData",
120
+ "GetData",
121
+ "routefor",
122
+ "XForwarded",
123
+ "subrequest",
124
+ "subrequest_bytes",
125
+ "subrequest_raw",
126
+ "object_or_404",
127
+ ]
@@ -33,7 +33,10 @@ from fresco.util.http import encode_multipart
33
33
  from fresco.util.urls import normpath, make_query
34
34
  from fresco.util.common import fq_path
35
35
  from fresco.util.wsgi import make_environ
36
- from fresco.typing import WSGICallable
36
+ from fresco.types import WSGIApplication
37
+ from fresco.types import HeaderList
38
+ from fresco.types import OptionalExcInfo
39
+ from fresco.types import WriteCallable
37
40
 
38
41
  from fresco.exceptions import ResponseException
39
42
  from fresco.requestcontext import context
@@ -46,10 +49,12 @@ from fresco.routing import (
46
49
  )
47
50
  from fresco.options import Options
48
51
 
49
- __all__ = ("FrescoApp", "urlfor")
52
+ __all__ = ("FrescoApp", "urlfor", "context")
50
53
 
51
54
  logger = logging.getLogger(__name__)
52
55
 
56
+ ExcInfo = tuple[t.Type[BaseException], BaseException, types.TracebackType]
57
+
53
58
 
54
59
  class FrescoApp(RouteCollection):
55
60
  """\
@@ -115,7 +120,9 @@ class FrescoApp(RouteCollection):
115
120
  #: If a function returns a value (other than ``None``),
116
121
  #: this value will be
117
122
  #: returned as the response instead of calling the scheduled view.
118
- self.process_http_error_response_handlers: List[Tuple[int, Callable]] = []
123
+ self.process_http_error_response_handlers: list[
124
+ tuple[t.Optional[int], Callable[[Request, Response], t.Optional[Response]]]
125
+ ] = []
119
126
 
120
127
  #: Functions to be called if an exception is raised during a view
121
128
  #: Each function will be passed ``request, exc_info``.
@@ -123,7 +130,9 @@ class FrescoApp(RouteCollection):
123
130
  #: this value will be
124
131
  #: returned as the response and the error will not be propagated.
125
132
  #: If all exception handlers return None then the error will be raised
126
- self.process_exception_handlers: List[Tuple[Exception, Callable]] = []
133
+ self.process_exception_handlers: list[
134
+ tuple[Type[Exception], Callable[[Request, ExcInfo], Union[Response, None]]]
135
+ ] = []
127
136
 
128
137
  #: Functions to be called at the end of request processing,
129
138
  #: after all content has been output.
@@ -151,12 +160,12 @@ class FrescoApp(RouteCollection):
151
160
 
152
161
  def get_response(
153
162
  self,
154
- request,
155
- path,
156
- method,
163
+ request: Request,
164
+ path: str,
165
+ method: str,
157
166
  currentcontext=context.currentcontext,
158
167
  normpath=normpath,
159
- ):
168
+ ) -> Response:
160
169
  ctx = currentcontext()
161
170
  ctx["app"] = self
162
171
  environ = request.environ
@@ -178,19 +187,19 @@ class FrescoApp(RouteCollection):
178
187
 
179
188
  try:
180
189
  for traversal in self.get_route_traversals(path, method, request):
190
+ route = traversal.route
181
191
  try:
182
- route = traversal.route
183
192
  environ["wsgiorg.routing_args"] = (
184
193
  traversal.args,
185
194
  traversal.kwargs,
186
195
  )
187
- view = route.getview(method)
196
+ view = traversal.view
188
197
  ctx["view_self"] = getattr(view, "__self__", None)
189
198
  ctx["route_traversal"] = traversal
190
199
  if self.logger:
191
200
  self.logger.info(
192
201
  "matched route: %s %r => %r",
193
- request.method,
202
+ method,
194
203
  path,
195
204
  fq_path(view),
196
205
  )
@@ -259,19 +268,21 @@ class FrescoApp(RouteCollection):
259
268
 
260
269
  # Is the URL just missing a trailing '/'?
261
270
  if not path or path[-1] != "/":
262
- for _ in self.get_methods(request, path + "/"):
271
+ if self.get_methods(request, path + "/"):
263
272
  return Response.unrestricted_redirect_permanent(path + "/")
264
273
 
265
274
  return Response.not_found()
266
275
 
267
- def view(self, request=None) -> Response:
276
+ def view(self, request: t.Optional[Request] = None) -> Response:
268
277
  request = request or context.request
269
278
  try:
270
279
  path = request.path_info
271
280
  except ResponseException as e:
272
281
  response = e.response
273
282
  else:
274
- response = self.get_response(request, path, request.method)
283
+ response = self.get_response(
284
+ request, path, request.environ["REQUEST_METHOD"]
285
+ )
275
286
 
276
287
  for f in self.process_response_handlers:
277
288
  try:
@@ -287,7 +298,9 @@ class FrescoApp(RouteCollection):
287
298
 
288
299
  return response
289
300
 
290
- def handle_http_error_response(self, request, response):
301
+ def handle_http_error_response(
302
+ self, request: Request, response: Response
303
+ ) -> Response:
291
304
  """
292
305
  Call any process_http_error_response handlers and return the
293
306
  (potentially modified) response object.
@@ -303,8 +316,8 @@ class FrescoApp(RouteCollection):
303
316
  self.log_exception(request)
304
317
  return response
305
318
 
306
- def get_methods(self, request, path):
307
- """\
319
+ def get_methods(self, request: Request, path: str) -> Set[str]:
320
+ """
308
321
  Return the HTTP methods valid in routes to the given path
309
322
  """
310
323
  methods: Set[str] = set()
@@ -322,12 +335,7 @@ class FrescoApp(RouteCollection):
322
335
  exc_info=exc_info,
323
336
  )
324
337
 
325
- def handle_exception(
326
- self, request, allow_reraise=True
327
- ) -> Union[
328
- Response,
329
- Tuple[Type[BaseException], BaseException, types.TracebackType],
330
- ]:
338
+ def handle_exception(self, request, allow_reraise=True) -> Response:
331
339
  exc_info = sys.exc_info()
332
340
  if exc_info[0] is None:
333
341
  raise AssertionError(
@@ -344,10 +352,7 @@ class FrescoApp(RouteCollection):
344
352
  # server handle it
345
353
  if allow_reraise and not have_error_handlers:
346
354
  raise exc_info[1].with_traceback(exc_info[2]) # type: ignore
347
- response: Union[
348
- Response,
349
- Tuple[Type[BaseException], BaseException, types.TracebackType],
350
- ] = Response.internal_server_error()
355
+ response: Response = Response.internal_server_error()
351
356
 
352
357
  if not self.process_exception_handlers:
353
358
  self.log_exception(request, exc_info)
@@ -404,7 +409,7 @@ class FrescoApp(RouteCollection):
404
409
  self.reset_wsgi_app()
405
410
  self._middleware.insert(position, (middleware, args, kwargs))
406
411
 
407
- def make_wsgi_app(self, wsgi_app=None, use_middleware=True) -> WSGICallable:
412
+ def make_wsgi_app(self, wsgi_app=None, use_middleware=True) -> WSGIApplication:
408
413
  """
409
414
  Return a WSGI (PEP-3333) compliant application that drives this
410
415
  FrescoApp object.
@@ -417,7 +422,7 @@ class FrescoApp(RouteCollection):
417
422
  """
418
423
  if wsgi_app is None:
419
424
 
420
- def wsgi_app(
425
+ def _wsgi_app(
421
426
  environ,
422
427
  start_response,
423
428
  view=self.view,
@@ -426,15 +431,18 @@ class FrescoApp(RouteCollection):
426
431
  request = request_class(environ)
427
432
  return view(request)(environ, start_response)
428
433
 
434
+ else:
435
+ _wsgi_app = wsgi_app
436
+
429
437
  if use_middleware:
430
438
  for m, m_args, m_kwargs in self._middleware:
431
- wsgi_app = m(wsgi_app, *m_args, **m_kwargs)
439
+ _wsgi_app = m(_wsgi_app, *m_args, **m_kwargs)
432
440
 
433
441
  def fresco_wsgi_app(
434
442
  environ,
435
443
  start_response,
436
444
  frescoapp=self,
437
- wsgi_app=wsgi_app,
445
+ wsgi_app=_wsgi_app,
438
446
  request_class=self.request_class,
439
447
  process_teardown_handlers=self.process_teardown_handlers,
440
448
  call_process_teardown_handlers=self.call_process_teardown_handlers,
@@ -600,7 +608,11 @@ class FrescoApp(RouteCollection):
600
608
  self.process_response_handlers.append(func)
601
609
  return func
602
610
 
603
- def process_exception(self, func, exc_type=Exception):
611
+ def process_exception(
612
+ self,
613
+ func: Callable[[Request, ExcInfo], Union[Response, None]],
614
+ exc_type: Type[Exception] = Exception,
615
+ ):
604
616
  """
605
617
  Register a ``process_exception`` hook function
606
618
  """
@@ -666,7 +678,9 @@ class FrescoApp(RouteCollection):
666
678
  start_response("200 OK", [])
667
679
  yield b""
668
680
 
669
- def fake_start_response(status, headers, exc_info=None):
681
+ def fake_start_response(
682
+ status: str, headers: HeaderList, exc_info: OptionalExcInfo = None
683
+ ) -> WriteCallable:
670
684
  return lambda s: None
671
685
 
672
686
  environ = make_environ(url, environ, wsgi_input, **kwargs)
@@ -691,12 +705,12 @@ class FrescoApp(RouteCollection):
691
705
  if multipart:
692
706
  wsgi_input, headers = encode_multipart(data, files)
693
707
  kwargs.update(headers)
694
- elif hasattr(data, "read"):
708
+ elif isinstance(data, t.BinaryIO):
695
709
  wsgi_input = data.read()
696
710
  elif isinstance(data, bytes):
697
711
  wsgi_input = data
698
712
  elif data is None:
699
- wsgi_input = ""
713
+ wsgi_input = b""
700
714
  else:
701
715
  wsgi_input = make_query(data).encode("ascii")
702
716
 
@@ -14,20 +14,23 @@
14
14
  #
15
15
  import sys
16
16
  from functools import wraps
17
+ import typing as t
17
18
 
18
19
  from fresco import Response
19
20
 
20
21
  _marker = object()
21
22
 
22
23
 
23
- def onerror(exceptions, handler):
24
- """\
24
+ def onerror(
25
+ exceptions: t.Union[t.Type[Exception], tuple[t.Type[Exception], ...]], handler
26
+ ) -> t.Callable:
27
+ """
25
28
  Return a decorator that can replace or update the return value of the
26
29
  function if an exception is raised
27
30
  """
28
31
 
29
32
  try:
30
- if isinstance(exceptions, Exception):
33
+ if isinstance(exceptions, type):
31
34
  exceptions = (exceptions,)
32
35
  except TypeError:
33
36
  pass
@@ -0,0 +1 @@
1
+ DEFAULT_CHARSET = "UTF-8"
@@ -12,11 +12,17 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
- __all__ = "XForwarded"
15
+ import typing as t
16
+
17
+ from fresco.types import WSGIApplication
18
+ from fresco.types import WSGIEnviron
19
+ from fresco.types import StartResponse
20
+
21
+ __all__ = ["XForwarded"]
16
22
 
17
23
 
18
24
  class XForwarded(object):
19
- """\
25
+ """
20
26
  Modify the WSGI environment so that the X_FORWARDED_* headers are observed
21
27
  and generated URIs are correct in a proxied environment.
22
28
 
@@ -33,6 +39,13 @@ class XForwarded(object):
33
39
  the wsgi.url_scheme is modified to ``https`` and ``HTTPS`` is set to
34
40
  ``on``.
35
41
 
42
+ :param trusted:
43
+ List of IP addresses trusted to set the HTTP_X_FORWARDED_* headers
44
+
45
+ :param force_https:
46
+ If True, the following environ keys will be set unconditionally:
47
+ ``"HTTPS": "on"`` and ``"wsgi.url_scheme": "https"`` will be set
48
+
36
49
  Example::
37
50
 
38
51
  >>> from fresco import FrescoApp, context, GET, Response
@@ -68,24 +81,36 @@ class XForwarded(object):
68
81
  u'URL is https://real-name/; REMOTE_ADDR is 1.2.3.4'
69
82
  """
70
83
 
71
- def __init__(self, app, trusted=None):
84
+ def __init__(
85
+ self,
86
+ app: WSGIApplication,
87
+ trusted: t.Optional[t.Iterable[str]] = None,
88
+ force_https: t.Optional[bool] = None,
89
+ ) -> None:
72
90
  self.app = app
91
+ self.force_https = force_https
73
92
  if trusted:
74
93
  self.trusted = set(trusted)
75
94
  else:
76
95
  self.trusted = set()
77
96
 
78
- def __call__(self, environ, start_response):
79
- """\
97
+ def __call__(
98
+ self, environ: WSGIEnviron, start_response: StartResponse
99
+ ) -> t.Iterable[bytes]:
100
+ """
80
101
  Call the WSGI app, passing it a modified environ
81
102
  """
82
103
  env = environ.get
83
- is_ssl = (
84
- env("HTTP_X_FORWARDED_PROTO") == "https"
85
- or env("HTTP_X_FORWARDED_SSL") == "on"
86
- )
87
104
 
105
+ if self.force_https is None:
106
+ is_ssl = (
107
+ env("HTTP_X_FORWARDED_PROTO") == "https"
108
+ or env("HTTP_X_FORWARDED_SSL") == "on"
109
+ )
110
+ else:
111
+ is_ssl = self.force_https
88
112
  host = env("HTTP_X_FORWARDED_HOST")
113
+
89
114
  if host is not None:
90
115
  if ":" in host:
91
116
  port = host.split(":")[1]
@@ -99,21 +124,17 @@ class XForwarded(object):
99
124
  environ["wsgi.url_scheme"] = "https"
100
125
  environ["HTTPS"] = "on"
101
126
 
102
- try:
103
- forwards = environ["HTTP_X_FORWARDED_FOR"].split(", ") + [
104
- env("REMOTE_ADDR", "")
105
- ]
106
- except KeyError:
107
- # No X-Forwarded-For header?
108
- return self.app(environ, start_response)
109
-
110
- if self.trusted:
111
- for ip in forwards[::-1]:
112
- # Find the first non-trusted ip; this is our remote address
113
- if ip not in self.trusted:
114
- environ["REMOTE_ADDR"] = ip
115
- break
116
- else:
117
- environ["REMOTE_ADDR"] = forwards[0]
127
+ forwarded_for = env("HTTP_X_FORWARDED_FOR")
128
+ if forwarded_for:
129
+ addrs = forwarded_for.split(", ")
130
+
131
+ if self.trusted:
132
+ for ip in addrs[::-1]:
133
+ # Find the first non-trusted ip; this is our remote address
134
+ if ip not in self.trusted:
135
+ environ["REMOTE_ADDR"] = ip
136
+ break
137
+ else:
138
+ environ["REMOTE_ADDR"] = addrs[0]
118
139
 
119
140
  return self.app(environ, start_response)