fresco 3.4.0__py3-none-any.whl → 3.5.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.

Potentially problematic release.


This version of fresco might be problematic. Click here for more details.

fresco/__init__.py CHANGED
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
- __version__ = "3.4.0"
15
+ __version__ = "3.5.0"
16
16
 
17
17
  DEFAULT_CHARSET = "UTF-8"
18
18
 
fresco/core.py CHANGED
@@ -46,7 +46,7 @@ from fresco.routing import (
46
46
  )
47
47
  from fresco.options import Options
48
48
 
49
- __all__ = ("FrescoApp", "urlfor")
49
+ __all__ = ("FrescoApp", "urlfor", "context")
50
50
 
51
51
  logger = logging.getLogger(__name__)
52
52
 
@@ -184,13 +184,13 @@ class FrescoApp(RouteCollection):
184
184
  traversal.args,
185
185
  traversal.kwargs,
186
186
  )
187
- view = route.getview(method)
187
+ view = traversal.view
188
188
  ctx["view_self"] = getattr(view, "__self__", None)
189
189
  ctx["route_traversal"] = traversal
190
190
  if self.logger:
191
191
  self.logger.info(
192
192
  "matched route: %s %r => %r",
193
- request.method,
193
+ method,
194
194
  path,
195
195
  fq_path(view),
196
196
  )
fresco/middleware.py CHANGED
@@ -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
+ import typing as t
16
+
17
+ from fresco.typing import WSGICallable
18
+ from fresco.typing import WSGIEnviron
19
+ from fresco.typing import StartResponse
20
+
15
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: WSGICallable,
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)
fresco/options.py CHANGED
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
+ import contextlib
15
16
  import inspect
17
+ import itertools
16
18
  import json
17
19
  import logging
18
20
  import typing as t
@@ -316,6 +318,45 @@ class Options(dict):
316
318
  self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
317
319
 
318
320
 
321
+ @contextlib.contextmanager
322
+ def override_options(options, other=None, **kwargs):
323
+ """
324
+ Context manager that updates the given Options object with new values.
325
+ On exit, the old values will be restored.
326
+
327
+ This function is provided to assist with writing tests. It directly
328
+ modifies the given options object and does not prevent other threads from
329
+ accessing the modified values.
330
+ """
331
+ saved: list[tuple[str, t.Any]] = []
332
+ items: t.Iterable[tuple[str, t.Any]] = []
333
+ if other is not None:
334
+
335
+ keys = getattr(other, "keys", None)
336
+ if keys and callable(keys):
337
+ items = ((k, other[k]) for k in keys())
338
+
339
+ if kwargs:
340
+ items = itertools.chain(items, kwargs.items())
341
+
342
+ NOT_PRESENT = object()
343
+
344
+ for k, v in items:
345
+ if k in options:
346
+ saved.append((k, options[k]))
347
+ else:
348
+ saved.append((k, NOT_PRESENT))
349
+
350
+ options[k] = v
351
+
352
+ yield options
353
+ for k, v in saved:
354
+ if v is NOT_PRESENT:
355
+ del options[k]
356
+ else:
357
+ options[k] = v
358
+
359
+
319
360
  def parse_value(
320
361
  options: Mapping,
321
362
  v: str,
fresco/py.typed ADDED
File without changes
fresco/routing.py CHANGED
@@ -14,6 +14,7 @@
14
14
  #
15
15
  import re
16
16
  import sys
17
+ import inspect
17
18
  import warnings
18
19
  from copy import copy
19
20
  from collections import defaultdict
@@ -38,6 +39,7 @@ from fresco.response import Response
38
39
  from fresco.request import Request
39
40
  from fresco.requestcontext import context
40
41
  from fresco.routeargs import RouteArg
42
+ from fresco.typing import WSGICallable
41
43
  from fresco.util.cache import make_cache
42
44
  from fresco.util.common import fq_path
43
45
  from fresco.util.urls import join_path
@@ -128,7 +130,7 @@ PathMatch = namedtuple(
128
130
 
129
131
 
130
132
  class RouteTraversal(
131
- namedtuple("RouteTraversal", "route args kwargs collections_traversed")
133
+ namedtuple("RouteTraversal", "route view args kwargs collections_traversed")
132
134
  ):
133
135
  """
134
136
  Encapsulate a route traversal.
@@ -137,6 +139,7 @@ class RouteTraversal(
137
139
 
138
140
  - ``route`` the final route traversed, allowing access to the view
139
141
  associated with the path.
142
+ - ``view``, the resolved view callable
140
143
  - ``args`` - positional args to be passed to the view. This is a
141
144
  combination of args extracted from the path and any added when
142
145
  constructing the route.
@@ -206,7 +209,7 @@ class RouteTraversal(
206
209
  #: An item of RouteTraversal.collections_traversed
207
210
  TraversedCollection = namedtuple(
208
211
  "TraversedCollection",
209
- "collection path route args kwargs " "traversal_args traversal_kwargs",
212
+ "collection path route args kwargs traversal_args traversal_kwargs",
210
213
  )
211
214
 
212
215
 
@@ -708,8 +711,7 @@ class Route(object):
708
711
  lambda: defaultdict(list)
709
712
  )
710
713
 
711
- #: Always provide an positional ``request`` argument to views
712
- provide_request = False
714
+ provide_request: Optional[bool] = None
713
715
 
714
716
  def __init__(
715
717
  self,
@@ -730,7 +732,9 @@ class Route(object):
730
732
  :param pattern: A string that can be compiled into a path pattern
731
733
  :param methods: The list of HTTP methods the view is bound to
732
734
  ('GET', 'POST', etc)
733
- :param view: The view function.
735
+ :param view:
736
+ The view function, or a string identifier which will later be resolved.
737
+
734
738
  :param kwargs: A dictionary of default keyword arguments to pass
735
739
  to the view callable
736
740
  :param args: Positional arguments to pass to the view callable
@@ -747,8 +751,11 @@ class Route(object):
747
751
  by a view will cause the current response to be
748
752
  discarded with routing continuing to the next
749
753
  available route.
750
- :param provide_request: If True, provide the current request as the
751
- first argument to the view callable.
754
+ :param provide_request:
755
+ If True, provide the current request as the first argument to the
756
+ view callable. Defaults to ``None``, which will autodetect if the
757
+ view function has an initial parameter of type
758
+ :class:`~fresco.request.Request`
752
759
  :param **_kwargs: Keyword arguments matching HTTP method names
753
760
  (GET, POST etc) can used to specify views
754
761
  associated with those methods.
@@ -904,14 +911,14 @@ class Route(object):
904
911
  newroute.fallthrough_statuses = {int(s) for s in status_codes}
905
912
  return newroute
906
913
 
907
- def match(self, path, method):
914
+ def match(self, path: str, method: t.Optional[str]) -> t.Optional[PathMatch]:
908
915
  if method and method not in self.methods:
909
916
  return None
910
917
  return self.pattern.match(path)
911
918
 
912
- def getview(self, method: str) -> Callable:
913
- """\
914
- Return the raw view callable.
919
+ def getview(self, method: str) -> Callable[..., Any]:
920
+ """
921
+ Resolve and return the raw view callable.
915
922
  """
916
923
  try:
917
924
  return self._cached_views[method]
@@ -931,6 +938,12 @@ class Route(object):
931
938
  uview = getattr(self.instance, uview)
932
939
 
933
940
  self._cached_views[method] = uview
941
+ if self.provide_request is None:
942
+ if callable(uview):
943
+ self.provide_request = _has_request_parameter(uview)
944
+ else:
945
+ self.provide_request = False
946
+
934
947
  return uview
935
948
 
936
949
  @classmethod
@@ -1394,8 +1407,8 @@ class RouteCollection(MutableSequence):
1394
1407
  raise exc
1395
1408
 
1396
1409
  def _get_routes(
1397
- self, key: Tuple[t.Optional[str], str]
1398
- ) -> t.Sequence[t.Tuple[Route, PathMatch]]:
1410
+ self, key: tuple[t.Optional[str], str]
1411
+ ) -> t.Sequence[tuple[Route, PathMatch]]:
1399
1412
  method, path = key
1400
1413
  routes = ((r, r.match(path, method)) for r in self.__routes__)
1401
1414
  return [(r, t) for (r, t) in routes if t is not None]
@@ -1437,6 +1450,12 @@ class RouteCollection(MutableSequence):
1437
1450
 
1438
1451
  # View function arguments extracted while traversing the path
1439
1452
  traversal_args, traversal_kwargs = result.args, result.kwargs
1453
+ if method is None:
1454
+ for m in route.viewspecs:
1455
+ view = route.getview(m)
1456
+ break
1457
+ else:
1458
+ view = route.getview(method)
1440
1459
 
1441
1460
  # Process any args/kwargs defined in the Route declaration.
1442
1461
  if request:
@@ -1468,7 +1487,7 @@ class RouteCollection(MutableSequence):
1468
1487
  raise exc
1469
1488
 
1470
1489
  r = self.route_class("/", ALL_METHODS, raiser)
1471
- yield RouteTraversal(r, (), {}, [(self, "", r)])
1490
+ yield RouteTraversal(r, r.getview("GET"), (), {}, [(self, "", r)])
1472
1491
  continue
1473
1492
 
1474
1493
  for sub in sub_routes.get_route_traversals(
@@ -1493,17 +1512,21 @@ class RouteCollection(MutableSequence):
1493
1512
  # Dynamic routes consume their arguments when creating the
1494
1513
  # sub RouteCollection.
1495
1514
  if route.dynamic:
1496
- yield RouteTraversal(sub.route, sub.args, sub.kwargs, traversed)
1515
+ yield RouteTraversal(
1516
+ sub.route, sub.view, sub.args, sub.kwargs, traversed
1517
+ )
1497
1518
  else:
1498
1519
  yield RouteTraversal(
1499
1520
  sub.route,
1521
+ sub.view,
1500
1522
  args + sub.args,
1501
- dict(kwargs, **sub.kwargs),
1523
+ kwargs | sub.kwargs,
1502
1524
  traversed,
1503
1525
  )
1504
1526
  else:
1505
1527
  yield RouteTraversal(
1506
1528
  route,
1529
+ view,
1507
1530
  args,
1508
1531
  kwargs,
1509
1532
  [
@@ -1572,7 +1595,7 @@ class RouteCollection(MutableSequence):
1572
1595
  def route_wsgi(
1573
1596
  self,
1574
1597
  path: str,
1575
- wsgiapp: t.Union[t.Callable, str],
1598
+ wsgiapp: t.Union[WSGICallable, str],
1576
1599
  rewrite_script_name: bool = True,
1577
1600
  *args,
1578
1601
  **kwargs,
@@ -1600,7 +1623,7 @@ class RouteCollection(MutableSequence):
1600
1623
  resolved_wsgi_app = None
1601
1624
 
1602
1625
  def fresco_wsgi_view(
1603
- request,
1626
+ request: Request,
1604
1627
  path=path,
1605
1628
  ws_path=ws_path,
1606
1629
  rewrite_script_name=rewrite_script_name,
@@ -1775,3 +1798,24 @@ def register_converter(name, registry=ExtensiblePattern):
1775
1798
  return cls
1776
1799
 
1777
1800
  return register_converter
1801
+
1802
+
1803
+ def _has_request_parameter(func: Callable[..., Any]) -> bool:
1804
+ """
1805
+ Return True if the given function has an initial parameter of type Request
1806
+ """
1807
+
1808
+ def _is_request_annotation(a: Any) -> bool:
1809
+ if isinstance(a, type) and issubclass(a, Request):
1810
+ return True
1811
+
1812
+ origin = t.get_origin(a)
1813
+ if origin is t.Union and any(
1814
+ _is_request_annotation(arg) for arg in t.get_args(a)
1815
+ ):
1816
+ return True
1817
+ return False
1818
+
1819
+ sig = inspect.signature(func)
1820
+ firstparam = next(iter(sig.parameters.values()), None)
1821
+ return bool(firstparam and _is_request_annotation(firstparam.annotation))
fresco/subrequests.py CHANGED
@@ -1,7 +1,5 @@
1
1
  from itertools import chain
2
2
  from typing import Any
3
- from typing import Dict
4
- from typing import Tuple
5
3
 
6
4
  from fresco.core import context
7
5
  from fresco.request import Request
@@ -262,8 +260,8 @@ def resolve_viewspec(viewspec, *args, **kwargs):
262
260
 
263
261
 
264
262
  def _get_args_for_route(
265
- route: Route, request: Request, args: Tuple, kwargs: Dict[str, Any]
266
- ) -> Tuple[Tuple, Dict[str, Any]]:
263
+ route: Route, request: Request, args: tuple[Any, ...], kwargs: dict[str, Any]
264
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
267
265
  """
268
266
  Return the args/kwargs required to pass to the view callable for ``route``.
269
267
  """
@@ -24,6 +24,7 @@ import sys
24
24
  import pytest
25
25
 
26
26
  from fresco.options import Options
27
+ from fresco.options import override_options
27
28
  from fresco.options import parse_key_value_pairs
28
29
  from fresco.options import dict_from_options
29
30
 
@@ -104,6 +105,25 @@ class TestOptions(object):
104
105
  assert isinstance(Options().copy(), Options)
105
106
 
106
107
 
108
+ class TestOverrideOptions:
109
+
110
+ def test_override_options_with_object(self):
111
+ options = Options(foo=1)
112
+ with override_options(options, {"foo": 2, "bar": "a"}):
113
+ assert options["foo"] == 2
114
+ assert options["bar"] == "a"
115
+ assert options["foo"] == 1
116
+ assert "bar" not in options
117
+
118
+ def test_override_options_with_kwargs(self):
119
+ options = Options(foo=1)
120
+ with override_options(options, foo=2, bar="a"):
121
+ assert options["foo"] == 2
122
+ assert options["bar"] == "a"
123
+ assert options["foo"] == 1
124
+ assert "bar" not in options
125
+
126
+
107
127
  class TestLoadKeyValuePairs:
108
128
  def test_it_loads_strings(self):
109
129
  assert parse_key_value_pairs({}, ["a=b"]) == {"a": "b"}
@@ -24,6 +24,7 @@ import tms
24
24
 
25
25
  from fresco import FrescoApp
26
26
  from fresco.core import urlfor
27
+ from fresco.request import Request
27
28
  from fresco.exceptions import NotFound
28
29
  from fresco.response import Response
29
30
  from fresco.routing import ALL_METHODS
@@ -31,6 +32,7 @@ from fresco.routing import ALL
31
32
  from fresco.routing import GET
32
33
  from fresco.routing import OPTIONS
33
34
  from fresco.routing import POST
35
+ from fresco.routing import _has_request_parameter
34
36
  from fresco.routing import (
35
37
  Route,
36
38
  DelegateRoute,
@@ -347,29 +349,29 @@ class TestPredicates(object):
347
349
 
348
350
  class TestRouteNames(object):
349
351
  def test_name_present_in_route_keys(self):
350
- r = Route("/", GET, None, name="foo")
352
+ r = Route("/", GET, lambda: None, name="foo")
351
353
  assert "foo" in list(r.route_keys())
352
354
 
353
355
  def test_name_with_other_kwargs(self):
354
- r = Route("/", GET, None, name="foo", x="bar")
356
+ r = Route("/", GET, lambda: None, name="foo", x="bar")
355
357
  assert "foo" in list(r.route_keys())
356
358
 
357
359
  def test_name_cannot_contain_colon(self):
358
360
  with pytest.raises(ValueError):
359
- Route("/", GET, None, name="foo:bar")
361
+ Route("/", GET, lambda: None, name="foo:bar")
360
362
 
361
363
 
362
364
  class TestRouteCollection(object):
363
365
  def test_it_adds_routes_from_constructor(self):
364
- r1 = Route("/1", GET, None, name="1")
365
- r2 = Route("/2", POST, None, name="2")
366
+ r1 = Route("/1", GET, lambda: None, name="1")
367
+ r2 = Route("/2", POST, lambda: None, name="2")
366
368
  rc = RouteCollection([r1, r2])
367
369
  assert [r.name for r in rc] == ["1", "2"]
368
370
 
369
371
  def test_it_adds_routecollections_from_constructor(self):
370
- r1 = Route("/", GET, None, name="1")
371
- r2 = Route("/", POST, None, name="2")
372
- r3 = Route("/", POST, None, name="3")
372
+ r1 = Route("/", GET, lambda: None, name="1")
373
+ r2 = Route("/", POST, lambda: None, name="2")
374
+ r3 = Route("/", POST, lambda: None, name="3")
373
375
  rc = RouteCollection([r1, RouteCollection([r2, r3])])
374
376
  assert [r.name for r in rc] == ["1", "2", "3"]
375
377
 
@@ -428,7 +430,7 @@ class TestRouteCollection(object):
428
430
  a.add_route(a_delegate_route)
429
431
  b.add_route(b_delegate_route)
430
432
 
431
- r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", None))
433
+ r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", "GET"))
432
434
 
433
435
  assert r.collections_traversed == [
434
436
  (a, "", a_delegate_route, (), {}, (), {}),
@@ -1141,3 +1143,51 @@ class TestRouteAll:
1141
1143
  assert len(list(app.get_route_traversals("/x", GET))) == 1
1142
1144
  assert len(list(app.get_route_traversals("/x/y", GET))) == 1
1143
1145
  assert len(list(app.get_route_traversals("/xy", GET))) == 0
1146
+
1147
+
1148
+ class TestRequestParamter:
1149
+
1150
+ def test_has_request_parameter(self):
1151
+
1152
+ def a(request: Request):
1153
+ pass
1154
+
1155
+ def b(request: t.Optional[Request]):
1156
+ pass
1157
+
1158
+ def c(request: t.Union[Request, None]):
1159
+ pass
1160
+
1161
+ def d(request: t.Union[None, Request]):
1162
+ pass
1163
+
1164
+ def e(request: int):
1165
+ pass
1166
+
1167
+ def f(request):
1168
+ pass
1169
+
1170
+ assert _has_request_parameter(a) is True
1171
+ assert _has_request_parameter(b) is True
1172
+ assert _has_request_parameter(c) is True
1173
+ assert _has_request_parameter(d) is True
1174
+ assert _has_request_parameter(e) is False
1175
+ assert _has_request_parameter(f) is False
1176
+
1177
+ def test_request_is_provided_automatically(self):
1178
+
1179
+ def a(request: Request):
1180
+ return Response("a")
1181
+
1182
+ def b():
1183
+ return Response("b")
1184
+
1185
+ app = FrescoApp()
1186
+ app.route("/a", GET=a)
1187
+ app.route("/b", GET=b)
1188
+
1189
+ with app.requestcontext("/a"):
1190
+ assert app.view().content == "a"
1191
+
1192
+ with app.requestcontext("/b"):
1193
+ assert app.view().content == "b"
fresco/typing.py CHANGED
@@ -1,11 +1,17 @@
1
1
  from types import TracebackType
2
2
  from typing import Any
3
3
  from typing import Callable
4
- from typing import Dict
5
4
  from typing import Iterable
6
- from typing import List
7
- from typing import Tuple
8
5
 
9
- WSGICallable = Callable[[Dict[str, Any], Callable], Iterable[bytes]]
10
- ExcInfoTuple = Tuple[type, BaseException, TracebackType]
11
- HeadersList = List[Tuple[str, str]]
6
+ HeaderList = list[tuple[str, str]]
7
+ HeadersList = HeaderList
8
+ WSGIEnviron = dict[str, Any]
9
+ StartResponse = Callable[[str, HeaderList], None]
10
+ WSGICallable = Callable[
11
+ [
12
+ WSGIEnviron,
13
+ StartResponse,
14
+ ],
15
+ Iterable[bytes]
16
+ ]
17
+ ExcInfoTuple = tuple[type, BaseException, TracebackType]
fresco/util/wsgi.py CHANGED
@@ -27,7 +27,7 @@ from typing import Tuple
27
27
  import sys
28
28
 
29
29
  from fresco.typing import ExcInfoTuple
30
- from fresco.typing import HeadersList
30
+ from fresco.typing import HeaderList
31
31
  from fresco.typing import WSGICallable
32
32
 
33
33
  logger = logging.getLogger(__name__)
@@ -407,16 +407,16 @@ def make_environ(url="/", environ=None, wsgi_input=b"", **kwargs):
407
407
 
408
408
  def apply_request(
409
409
  request, wsgicallable: WSGICallable
410
- ) -> Tuple[str, HeadersList, Optional[ExcInfoTuple], List[bytes]]:
410
+ ) -> Tuple[str, HeaderList, Optional[ExcInfoTuple], List[bytes]]:
411
411
  """
412
412
  Execute ``wsgicallable`` with the given request, exhaust and close the
413
413
  content iterator and return the result.
414
414
  """
415
415
 
416
- _start_response_result: List[Tuple[str, HeadersList, Optional[ExcInfoTuple]]] = []
416
+ _start_response_result: List[Tuple[str, HeaderList, Optional[ExcInfoTuple]]] = []
417
417
 
418
418
  def start_response(
419
- status: str, headers: HeadersList, exc_info: Optional[ExcInfoTuple] = None
419
+ status: str, headers: HeaderList, exc_info: Optional[ExcInfoTuple] = None
420
420
  ):
421
421
  _start_response_result.append((status, headers, exc_info))
422
422
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fresco
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: A Web/WSGI micro-framework
5
5
  Author-email: Oliver Cope <oliver@redgecko.org>
6
6
  License: Apache
@@ -1,20 +1,21 @@
1
- fresco/__init__.py,sha256=rteQgLzbeRKgdSkiDL_0ey72k3neyI5FtDIG8LR9vxA,3520
1
+ fresco/__init__.py,sha256=qd3y9P7W77xtuDx9WtYQt7wIHeRNUobog6zq8W4J-zg,3520
2
2
  fresco/cookie.py,sha256=Qnx8yOjU4LUJ1fqi7YvqbhAA01rCsclJGl_fxI68slw,7055
3
- fresco/core.py,sha256=mwYR6UP0zRSjcFlNZf51-otX9EcmvgjEZ7GDVQoQexg,26615
3
+ fresco/core.py,sha256=kBf_JY8wqvBSQzxP0pFUr0sOnvUoDltittL-5xDUlC8,26611
4
4
  fresco/decorators.py,sha256=84NUpRJ-M7GK6wDl42bmCRSvgoWzCsy1huyvGnSAPPw,3232
5
5
  fresco/exceptions.py,sha256=KE-LoYUGnho6KltzkU6cnm9vUiUhAiDIjPqn5ba-YCA,4410
6
- fresco/middleware.py,sha256=uCAuOtBnvaVBJGrQa8ecvLkSZ6aSKmWaJqxB8Rv4SsQ,4173
6
+ fresco/middleware.py,sha256=XMpakA1YMZJqwHQyyxgw6TFhecDRTrUSSeNLDEkY8qA,4808
7
7
  fresco/multidict.py,sha256=0CaNNIcFnZ1hLk3NExhNvjc_BtK4zVB26L9gP_7MeNM,13362
8
- fresco/options.py,sha256=Z1EZPRoEAqYHm2hg7gRmLtMWMKWjNgGVehPtCU66mBE,14068
8
+ fresco/options.py,sha256=gzFzTv6vib16WazRxT1s2xD91f8IRfCGKijx3BbE1gU,15163
9
+ fresco/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
10
  fresco/request.py,sha256=dC7pMg-4Kxa6PcqaaWFL4JviANQotuBjoKwlxZYywRY,27048
10
11
  fresco/requestcontext.py,sha256=P-SkKJkKLYVqNiR2zwooRROnSnE2VMj2P2-eD5DW5Qg,3504
11
12
  fresco/response.py,sha256=ADKHbtAGyhwtaUJxB7m_1nqVdZRKUryebmG4FDUjZVY,37072
12
13
  fresco/routeargs.py,sha256=dxNlqbQ1FrgIY6OCFzcEMdZ0OVyjlMQdQGLmUJgdPYU,10176
13
- fresco/routing.py,sha256=S33ysM6BJEKxWZh7mdPO6-Eep2t1s0Q_qlrev84iNIY,58977
14
+ fresco/routing.py,sha256=BfUaC9xD9BL_RqRPMIkMNOdLbhbSgzWSJWooktRoeqc,60515
14
15
  fresco/static.py,sha256=9SKQ3P1YFTP45Qiic-ycvkpKRvqIURp1XSzPazTmYLI,2513
15
- fresco/subrequests.py,sha256=wFJWLuhVAcei5imYc5NBSCBaHBEm-X4_XswPtK3O4Zw,11081
16
+ fresco/subrequests.py,sha256=zQlKJRZJVbfkxc0cQp3qoBFZH9pPFq77DgnYAJJvgAI,11052
16
17
  fresco/types.py,sha256=UHITRMDoS90s2zV2wNpqLFhRWESfaBAMuQnL4c21mqY,106
17
- fresco/typing.py,sha256=jQ1r_wme54J08XZUdmuuW4M8ve-gEhm1Wriwctls3r8,347
18
+ fresco/typing.py,sha256=uQWLElgVCJSZ3X2OkGNu-1ihmPvytyqSLguPB6tm-Pc,412
18
19
  fresco/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  fresco/tests/fixtures.py,sha256=eyo2zPivB3fItDkrJqWnOCvIS_A1q1JEhT4AutAB--o,1871
20
21
  fresco/tests/test_cookie.py,sha256=HTLmNCjcPoZDeFnZAzY3lJPeduzgU4mi9C-74eSQCec,2171
@@ -23,12 +24,12 @@ fresco/tests/test_decorators.py,sha256=VFXHo1gm2jldQXeaEF3NNo5fnpdJ-LXc8-vNymPJK
23
24
  fresco/tests/test_exceptions.py,sha256=R0Tn86m33iTKecZ69TgH4CqY9XSFP0FsLMH10O5Jth8,973
24
25
  fresco/tests/test_middleware.py,sha256=D_sWfX-w3bhItOm54nB_cuYPGoWopISvZCFIuMX68cU,3137
25
26
  fresco/tests/test_multidict.py,sha256=uDwDYII0dvVxaEyDO85zRTWlIWw3LO3StzYemJVm0E0,7565
26
- fresco/tests/test_options.py,sha256=Zx1tLVLeok8tO3dWua2PyTPZgeWbXFMXtN3FnorBmC8,10998
27
+ fresco/tests/test_options.py,sha256=zKJveuxTz4HL9NeQYwe8qaki8WQw65ghhW3cLnXfaVI,11659
27
28
  fresco/tests/test_request.py,sha256=hoANrergrohlAeTbQufDMfIbvYURsPyPjCxOVKz7bVo,16389
28
29
  fresco/tests/test_requestcontext.py,sha256=t8hm-lzIk85ryb3sdlpVoPQyLDWpCjB86dg8nVG1yRw,3115
29
30
  fresco/tests/test_response.py,sha256=MrhHIDg81pJlTeEcn2rGtU-i59s1KzEccF81u4Up6xs,8934
30
31
  fresco/tests/test_routeargs.py,sha256=VMWUbrXegTLN9Tx2AcrpbjAAEaxAANzkcy02SmpFOmY,8162
31
- fresco/tests/test_routing.py,sha256=nOg3daHL8UigPEykHzgLKmlTOYtD4n_VjoBVfMsQcYo,38565
32
+ fresco/tests/test_routing.py,sha256=DkHP518f6UVAuG2pNVAyoyEcc2ej8T-Kw-moqkMB9gI,39851
32
33
  fresco/tests/test_static.py,sha256=y73dqzE2flpACQ_dvNhDzk_WlvNQfkhYF_8YY4MMGDo,4686
33
34
  fresco/tests/test_subrequests.py,sha256=7rluJnw-elXRXfrzvAQvGBHRBU93zwnL827mTxBGd3Y,7909
34
35
  fresco/tests/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -49,9 +50,9 @@ fresco/util/object.py,sha256=FjYNfPHzvBqq1rn0Y6As-2AVZ_SZOjH-lrSy4EbYmHY,370
49
50
  fresco/util/security.py,sha256=nXEdoCak_2c4OA1L1wGwhZygS22s2fzwR0Kp-DdwKZg,1058
50
51
  fresco/util/textproc.py,sha256=e5jLTofKCqdm6_Fy8XOyR43AJr5APtL59Kd8cNA9PrQ,2309
51
52
  fresco/util/urls.py,sha256=aaVovLyXNlVoGviiLN94ImqXf-LTQs_xooEIyi3LBc4,9195
52
- fresco/util/wsgi.py,sha256=2cr2b92RpvPSgS_P46X2mlYepoFg9Qji-V-fzxpSNzw,12971
53
- fresco-3.4.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
54
- fresco-3.4.0.dist-info/METADATA,sha256=Kk7LvAhriUF3HHAIb0ZFoTyADsgD_Czx3arDrYQCVoU,1549
55
- fresco-3.4.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
56
- fresco-3.4.0.dist-info/top_level.txt,sha256=p_1aMce5Shjq9fIfdbB-aN8wCDhjF_iYnn98bUebbII,7
57
- fresco-3.4.0.dist-info/RECORD,,
53
+ fresco/util/wsgi.py,sha256=gllmrw9um9ecs_sFxXy-Uwh7M9YAdcBNw33S63AhB8E,12967
54
+ fresco-3.5.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
55
+ fresco-3.5.0.dist-info/METADATA,sha256=mL1SE-OBfPOlYjc7YBSbt6ftilgaol-pUNAYqt3k0A0,1549
56
+ fresco-3.5.0.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
57
+ fresco-3.5.0.dist-info/top_level.txt,sha256=p_1aMce5Shjq9fIfdbB-aN8wCDhjF_iYnn98bUebbII,7
58
+ fresco-3.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.8.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5