fresco 3.5.0__py3-none-any.whl → 3.6.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 +55 -56
- fresco/core.py +33 -23
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +4 -4
- fresco/multidict.py +35 -51
- fresco/options.py +146 -75
- fresco/request.py +155 -34
- fresco/requestcontext.py +3 -0
- fresco/response.py +12 -9
- fresco/routeargs.py +23 -9
- fresco/routing.py +74 -56
- fresco/static.py +1 -1
- fresco/subrequests.py +1 -1
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +40 -16
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_routing.py +36 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +13 -11
- fresco/util/wsgi.py +15 -14
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/METADATA +3 -2
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/RECORD +30 -30
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -17
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/top_level.txt +0 -0
fresco/options.py
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
#
|
|
15
15
|
import contextlib
|
|
16
|
+
import dataclasses
|
|
16
17
|
import inspect
|
|
17
18
|
import itertools
|
|
18
19
|
import json
|
|
@@ -20,6 +21,7 @@ import logging
|
|
|
20
21
|
import typing as t
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
24
|
+
from collections.abc import Mapping
|
|
23
25
|
from decimal import Decimal
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
from socket import gethostname
|
|
@@ -28,7 +30,6 @@ from typing import Callable
|
|
|
28
30
|
from typing import Dict
|
|
29
31
|
from typing import Iterable
|
|
30
32
|
from typing import List
|
|
31
|
-
from typing import Mapping
|
|
32
33
|
from typing import Sequence
|
|
33
34
|
from typing import Union
|
|
34
35
|
|
|
@@ -37,6 +38,8 @@ from fresco.exceptions import OptionsLoadedException
|
|
|
37
38
|
__all__ = ["Options"]
|
|
38
39
|
|
|
39
40
|
logger = logging.getLogger(__name__)
|
|
41
|
+
known_suffixes = {"py", "sh", "rc", "txt", "cfg", "ini", "json", "toml", "conf"}
|
|
42
|
+
priority_pattern = re.compile(r"([0-9]+)(?:-(.*))?$")
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
class Options(dict):
|
|
@@ -98,7 +101,7 @@ class Options(dict):
|
|
|
98
101
|
strict=True,
|
|
99
102
|
dir=None,
|
|
100
103
|
trigger_onload=True,
|
|
101
|
-
):
|
|
104
|
+
) -> "Options":
|
|
102
105
|
"""
|
|
103
106
|
Find all files matching glob pattern ``sources`` and populates the
|
|
104
107
|
options object from those with matching filenames containing ``tags``.
|
|
@@ -121,20 +124,43 @@ class Options(dict):
|
|
|
121
124
|
format. Any other files will be interpreted as simple lists of
|
|
122
125
|
```key=value`` pairs.
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
Filename format
|
|
128
|
+
---------------
|
|
129
|
+
|
|
130
|
+
The general format of filenames is:
|
|
131
|
+
|
|
132
|
+
.. code-block:: shell
|
|
133
|
+
|
|
134
|
+
<base>(.<priority number>-)(<tags>)(<suffix>)
|
|
135
|
+
|
|
136
|
+
Example filenames:
|
|
137
|
+
|
|
138
|
+
.. code-block:: shell
|
|
139
|
+
|
|
140
|
+
# Just <base>
|
|
141
|
+
.env
|
|
142
|
+
|
|
143
|
+
# <base>.<suffix>
|
|
144
|
+
.settings.toml
|
|
145
|
+
|
|
146
|
+
# <base>.<tags>.<suffix>
|
|
147
|
+
.env.dev.local..py
|
|
148
|
+
|
|
149
|
+
# <base>.<priority>-<tags>.<suffix>
|
|
150
|
+
.env.100-dev.py
|
|
151
|
+
|
|
152
|
+
Priority number, if specified, is used to determine loading order.
|
|
153
|
+
Lower numbers are loaded first. Priority numbers must be positive
|
|
154
|
+
integers.
|
|
155
|
+
|
|
156
|
+
Tags are delimited with periods,
|
|
157
|
+
for example ``.env.production.py``.
|
|
126
158
|
The filename ``setttings.dev.local.ini`` would be
|
|
127
159
|
considered to have the tags ``('dev', 'local')``
|
|
128
160
|
|
|
129
161
|
Where filename contain multiple tags, all tags must match for the file
|
|
130
162
|
to be loaded.
|
|
131
163
|
|
|
132
|
-
Files are processed in the order that tags are specified in the
|
|
133
|
-
``tags`` parameter, and then in lexicographical order.
|
|
134
|
-
For example, calling ``options.load(..., tags=["dev", "local"])`` would
|
|
135
|
-
cause a file named "settings.dev" to be loaded before one named
|
|
136
|
-
"settings.local".
|
|
137
|
-
|
|
138
164
|
Tag names may contain the names of environment variable surrounded by
|
|
139
165
|
braces, for example ``{USER}``. These will be substituted for the
|
|
140
166
|
environment variable's value, with any dots or path separators replaced
|
|
@@ -145,8 +171,24 @@ class Options(dict):
|
|
|
145
171
|
|
|
146
172
|
Files with the suffix ".sample" are unconditionally excluded.
|
|
147
173
|
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
Loading order
|
|
175
|
+
-------------
|
|
176
|
+
|
|
177
|
+
Files are loaded in the following order:
|
|
178
|
+
|
|
179
|
+
1. On priority number, from low to high. If the priority number is not
|
|
180
|
+
given, a priority of zero is assumed
|
|
181
|
+
|
|
182
|
+
2. Then in tag order, based on the ordering given in the
|
|
183
|
+
``tags`` parameter
|
|
184
|
+
|
|
185
|
+
For example, calling ``options.load(..., tags=["dev", "local"])`` would
|
|
186
|
+
cause a file named "settings.dev" to be loaded before one named
|
|
187
|
+
"settings.local".
|
|
188
|
+
|
|
189
|
+
3. Finally in lexicographical order.
|
|
190
|
+
|
|
191
|
+
Environment variables, if requested, are loaded last.
|
|
150
192
|
|
|
151
193
|
Example::
|
|
152
194
|
|
|
@@ -154,19 +196,12 @@ class Options(dict):
|
|
|
154
196
|
opts.load(".env*", ["dev", "host-{hostname}", "local"])
|
|
155
197
|
|
|
156
198
|
Would load options from files named ``.env``, ``.env.json``, ``.env.dev.py``
|
|
157
|
-
and ``.env.local.py
|
|
199
|
+
and ``.env.local.py``, in that order.
|
|
158
200
|
|
|
159
201
|
"""
|
|
160
202
|
if self._is_loaded:
|
|
161
203
|
raise OptionsLoadedException("Options have already been loaded")
|
|
162
204
|
|
|
163
|
-
tag_substitutions = os.environ.copy()
|
|
164
|
-
tag_substitutions["hostname"] = gethostname()
|
|
165
|
-
tag_substitutions = {
|
|
166
|
-
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
167
|
-
for k, v in tag_substitutions.items()
|
|
168
|
-
}
|
|
169
|
-
|
|
170
205
|
candidates: List[Path] = []
|
|
171
206
|
if dir is None:
|
|
172
207
|
dir = Path(".")
|
|
@@ -183,52 +218,45 @@ class Options(dict):
|
|
|
183
218
|
if p.suffix.lower() != ".sample"
|
|
184
219
|
)
|
|
185
220
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
tagged_sources,
|
|
221
|
-
key=(
|
|
222
|
-
lambda ts: (
|
|
223
|
-
([], ts[1])
|
|
224
|
-
if len(ts[0]) == 0 else
|
|
225
|
-
(sorted(tags.index(t) for t in ts[0]), ts[1])
|
|
226
|
-
)
|
|
227
|
-
)
|
|
221
|
+
tag_substitutions = make_tag_substitutions()
|
|
222
|
+
subbed_tags: list[str] = []
|
|
223
|
+
for tag in tags:
|
|
224
|
+
try:
|
|
225
|
+
subbed_tags.append(tag.format(**tag_substitutions))
|
|
226
|
+
except KeyError:
|
|
227
|
+
pass
|
|
228
|
+
tags = subbed_tags
|
|
229
|
+
tagged_sources = []
|
|
230
|
+
for path in candidates:
|
|
231
|
+
priority = 0
|
|
232
|
+
filename = path.name
|
|
233
|
+
path_tags = []
|
|
234
|
+
path_tags = [t for t in str(filename).split(".") if t][1:]
|
|
235
|
+
if len(path_tags) > 0:
|
|
236
|
+
# Ignore the final tag if it matches a common config file
|
|
237
|
+
# extension
|
|
238
|
+
if path_tags[-1].lower() in known_suffixes:
|
|
239
|
+
path_tags.pop()
|
|
240
|
+
|
|
241
|
+
if path_tags:
|
|
242
|
+
if m := priority_pattern.match(path_tags[0]):
|
|
243
|
+
priority = int(m.group(1), 10)
|
|
244
|
+
if m.group(2):
|
|
245
|
+
path_tags[0] = m.group(2)
|
|
246
|
+
else:
|
|
247
|
+
path_tags = path_tags[1:]
|
|
248
|
+
|
|
249
|
+
if all(t in tags for t in path_tags):
|
|
250
|
+
tagged_sources.append(TaggedSource(priority, path_tags, path))
|
|
251
|
+
else:
|
|
252
|
+
excluded = [t for t in path_tags if t not in tags]
|
|
253
|
+
logger.debug(
|
|
254
|
+
f"Ignoring {path} as one or more tag does not match: {excluded=}"
|
|
228
255
|
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
|
|
257
|
+
matched = [
|
|
258
|
+
ts.path for ts in sorted(tagged_sources, key=tagged_source_sort_key(tags))
|
|
259
|
+
]
|
|
232
260
|
|
|
233
261
|
for path in matched:
|
|
234
262
|
existing_keys = set(self.keys())
|
|
@@ -236,7 +264,7 @@ class Options(dict):
|
|
|
236
264
|
if path.suffix == ".py":
|
|
237
265
|
self.update_from_file(str(path))
|
|
238
266
|
elif path.suffix == ".toml":
|
|
239
|
-
import toml
|
|
267
|
+
import toml # type: ignore
|
|
240
268
|
|
|
241
269
|
with path.open("r") as f:
|
|
242
270
|
self.update(toml.load(f))
|
|
@@ -264,9 +292,9 @@ class Options(dict):
|
|
|
264
292
|
)
|
|
265
293
|
|
|
266
294
|
if use_environ:
|
|
267
|
-
|
|
268
|
-
if k in os.environ
|
|
269
|
-
|
|
295
|
+
self |= {
|
|
296
|
+
k: parse_value(self, os.environ[k]) for k in self if k in os.environ
|
|
297
|
+
}
|
|
270
298
|
|
|
271
299
|
if trigger_onload:
|
|
272
300
|
self.do_loaded_callbacks()
|
|
@@ -282,9 +310,11 @@ class Options(dict):
|
|
|
282
310
|
:param load_all: If true private symbols will also be loaded into the
|
|
283
311
|
options object.
|
|
284
312
|
"""
|
|
285
|
-
ns: Dict[str, Any] = {"__file__": path}
|
|
313
|
+
ns: Dict[str, Any] = {"__file__": path, "options": self}
|
|
286
314
|
with open(path) as f:
|
|
287
315
|
exec(f.read(), ns)
|
|
316
|
+
if ns.get("options") is self:
|
|
317
|
+
del ns["options"]
|
|
288
318
|
self.update_from_dict(ns, load_all)
|
|
289
319
|
|
|
290
320
|
def update_from_dict(self, d, load_all=False):
|
|
@@ -330,11 +360,8 @@ def override_options(options, other=None, **kwargs):
|
|
|
330
360
|
"""
|
|
331
361
|
saved: list[tuple[str, t.Any]] = []
|
|
332
362
|
items: t.Iterable[tuple[str, t.Any]] = []
|
|
333
|
-
if other
|
|
334
|
-
|
|
335
|
-
keys = getattr(other, "keys", None)
|
|
336
|
-
if keys and callable(keys):
|
|
337
|
-
items = ((k, other[k]) for k in keys())
|
|
363
|
+
if isinstance(other, Mapping):
|
|
364
|
+
items = ((k, other[k]) for k in other.keys())
|
|
338
365
|
|
|
339
366
|
if kwargs:
|
|
340
367
|
items = itertools.chain(items, kwargs.items())
|
|
@@ -459,3 +486,47 @@ def dict_from_options(
|
|
|
459
486
|
if recursive:
|
|
460
487
|
return recursive_split(d, prefix[-1])
|
|
461
488
|
return d
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclasses.dataclass
|
|
492
|
+
class TaggedSource:
|
|
493
|
+
priority: int
|
|
494
|
+
tags: list[str]
|
|
495
|
+
path: Path
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def tagged_source_sort_key(
|
|
499
|
+
tags: list[str],
|
|
500
|
+
) -> Callable[[TaggedSource], tuple[int, list[int], str]]:
|
|
501
|
+
"""
|
|
502
|
+
Return a function that can be used as the ``key`` argument of``sorted``,
|
|
503
|
+
that sorts TaggedSource objects based on
|
|
504
|
+
priority, tag order, then filename.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
def _sort_key(ts: TaggedSource) -> tuple[int, list[int], str]:
|
|
508
|
+
sorted_tags = [tags.index(t) for t in ts.tags]
|
|
509
|
+
return (ts.priority, sorted_tags, str(ts.path))
|
|
510
|
+
|
|
511
|
+
return _sort_key
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def make_tag_substitutions():
|
|
515
|
+
"""
|
|
516
|
+
Return a dict of substitutions to make in tag names, based on the current
|
|
517
|
+
hostname and environment variables
|
|
518
|
+
|
|
519
|
+
This permits tags to be specified as, for example,
|
|
520
|
+
'{hostname}' or '{FRESCO_PROFILE}'
|
|
521
|
+
and the associated value will be substituted in.
|
|
522
|
+
|
|
523
|
+
Path separators and dots (which separate tags) are replaced in substitution
|
|
524
|
+
values by an underscore.
|
|
525
|
+
"""
|
|
526
|
+
substitutions = os.environ.copy()
|
|
527
|
+
substitutions["hostname"] = gethostname()
|
|
528
|
+
substitutions = {
|
|
529
|
+
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
530
|
+
for k, v in substitutions.items()
|
|
531
|
+
}
|
|
532
|
+
return substitutions
|
fresco/request.py
CHANGED
|
@@ -23,6 +23,7 @@ from urllib.parse import ParseResult
|
|
|
23
23
|
from urllib.parse import quote
|
|
24
24
|
from urllib.parse import urlparse
|
|
25
25
|
from urllib.parse import urlunparse
|
|
26
|
+
from decimal import Decimal
|
|
26
27
|
import typing as t
|
|
27
28
|
import datetime
|
|
28
29
|
import json
|
|
@@ -32,6 +33,7 @@ import re
|
|
|
32
33
|
from fresco import exceptions
|
|
33
34
|
from fresco.cookie import parse_cookie_header
|
|
34
35
|
from fresco.multidict import MultiDict
|
|
36
|
+
from fresco.defaults import DEFAULT_CHARSET
|
|
35
37
|
from fresco.types import QuerySpec
|
|
36
38
|
from fresco.util.http import FileUpload
|
|
37
39
|
from fresco.util.http import get_body_bytes
|
|
@@ -48,7 +50,15 @@ __all__ = "Request", "currentrequest"
|
|
|
48
50
|
KB = 1024
|
|
49
51
|
MB = 1024 * KB
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
|
|
54
|
+
class _Marker:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_marker = _Marker()
|
|
59
|
+
|
|
60
|
+
T1 = t.TypeVar("T1")
|
|
61
|
+
T2 = t.TypeVar("T2")
|
|
52
62
|
|
|
53
63
|
|
|
54
64
|
class Request(object):
|
|
@@ -86,7 +96,7 @@ class Request(object):
|
|
|
86
96
|
environ: t.Dict
|
|
87
97
|
|
|
88
98
|
#: Encoding used to decode WSGI parameters, notably PATH_INFO and form data
|
|
89
|
-
default_charset =
|
|
99
|
+
default_charset = DEFAULT_CHARSET
|
|
90
100
|
|
|
91
101
|
#: The decoder class to use for JSON request payloads
|
|
92
102
|
json_decoder_class = json.JSONDecoder
|
|
@@ -196,7 +206,9 @@ class Request(object):
|
|
|
196
206
|
by the client.
|
|
197
207
|
"""
|
|
198
208
|
try:
|
|
199
|
-
return self.json_decoder_class(*args, **kwargs).decode(
|
|
209
|
+
return self.json_decoder_class(*args, **kwargs).decode(
|
|
210
|
+
self.body # type: ignore
|
|
211
|
+
)
|
|
200
212
|
except ValueError:
|
|
201
213
|
raise exceptions.RequestParseError("Payload is not valid JSON")
|
|
202
214
|
|
|
@@ -235,35 +247,64 @@ class Request(object):
|
|
|
235
247
|
|
|
236
248
|
return self._query
|
|
237
249
|
|
|
238
|
-
def __getitem__(self, key
|
|
250
|
+
def __getitem__(self, key: str) -> str:
|
|
239
251
|
"""
|
|
240
252
|
Return the value of ``key`` from submitted form values.
|
|
241
253
|
"""
|
|
242
|
-
v = self.get(key,
|
|
243
|
-
if v is
|
|
254
|
+
v = self.get(key, default=None)
|
|
255
|
+
if v is None:
|
|
244
256
|
raise KeyError(key)
|
|
245
257
|
return v
|
|
246
258
|
|
|
247
|
-
|
|
259
|
+
@t.overload
|
|
260
|
+
def get(
|
|
261
|
+
self, key: str, default: T1, type: t.Callable[[str], T2] = str
|
|
262
|
+
) -> t.Union[T1, T2, None]:
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
@t.overload
|
|
266
|
+
def get(self, key: str, *, type: t.Callable[[str], T2]) -> t.Union[T2, None]:
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
@t.overload
|
|
270
|
+
def get(
|
|
271
|
+
self, key: str, *, type: t.Callable[[str], T2] = str, required: t.Literal[True]
|
|
272
|
+
) -> T2:
|
|
273
|
+
...
|
|
274
|
+
|
|
275
|
+
def get(
|
|
276
|
+
self,
|
|
277
|
+
key: str,
|
|
278
|
+
default=_marker,
|
|
279
|
+
type: t.Callable[[str], T2] = str,
|
|
280
|
+
required: bool = False,
|
|
281
|
+
):
|
|
248
282
|
"""
|
|
249
283
|
Look up ``key`` in submitted form values.
|
|
250
284
|
|
|
251
|
-
:param type:
|
|
252
|
-
|
|
285
|
+
:param type:
|
|
286
|
+
The type to which the returned result should be converted
|
|
287
|
+
|
|
288
|
+
A :class:`fresco.exceptions.BadRequest` error is raised if ``type`` is
|
|
289
|
+
specified and the value cannot be converted to the given type
|
|
290
|
+
|
|
291
|
+
:param default:
|
|
292
|
+
The value to return if the key not present.
|
|
293
|
+
|
|
294
|
+
:param required:
|
|
295
|
+
If True, a missing key will cause a
|
|
296
|
+
:class:`fresco.exceptions.BadRequest` to be raised.
|
|
253
297
|
|
|
254
|
-
If ``type`` is specified and the value cannot be converted to the given
|
|
255
|
-
type, or the value is not present and and no default value has been
|
|
256
|
-
specified, then a :class:`fresco.exceptions.BadRequest` error is raised.
|
|
257
298
|
"""
|
|
258
|
-
|
|
259
|
-
value
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if type is None:
|
|
299
|
+
value = self.form.get(key, _marker)
|
|
300
|
+
if value is _marker:
|
|
301
|
+
if required:
|
|
302
|
+
raise exceptions.BadRequest()
|
|
303
|
+
if default is _marker:
|
|
264
304
|
return None
|
|
305
|
+
return default
|
|
265
306
|
|
|
266
|
-
if type is
|
|
307
|
+
if type is str:
|
|
267
308
|
return value
|
|
268
309
|
try:
|
|
269
310
|
return type(value)
|
|
@@ -276,15 +317,97 @@ class Request(object):
|
|
|
276
317
|
"""
|
|
277
318
|
return self.form.getlist(key)
|
|
278
319
|
|
|
279
|
-
|
|
320
|
+
@t.overload
|
|
321
|
+
def getbool(self, key: str) -> t.Union[bool, None]:
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
@t.overload
|
|
325
|
+
def getbool(self, key: str, default: T1) -> t.Union[bool, T1, None]:
|
|
326
|
+
...
|
|
327
|
+
|
|
328
|
+
@t.overload
|
|
329
|
+
def getbool(self, key: str, *, required: t.Literal[True]) -> bool:
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
def getbool(
|
|
333
|
+
self, key: str, default: T1 = _marker, required: bool = False
|
|
334
|
+
) -> t.Union[bool, T1, None]:
|
|
280
335
|
"""
|
|
281
|
-
Return the named key, converted to
|
|
336
|
+
Return the named key, converted to a bool.
|
|
337
|
+
"""
|
|
338
|
+
if required:
|
|
339
|
+
return self.get(key, type=bool, required=True)
|
|
340
|
+
else:
|
|
341
|
+
return self.get(key, default=default, type=bool)
|
|
342
|
+
|
|
343
|
+
@t.overload
|
|
344
|
+
def getdecimal(self, key: str) -> t.Union[Decimal, None]:
|
|
345
|
+
...
|
|
346
|
+
|
|
347
|
+
@t.overload
|
|
348
|
+
def getdecimal(self, key: str, default: T1) -> t.Union[Decimal, T1, None]:
|
|
349
|
+
...
|
|
282
350
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
351
|
+
@t.overload
|
|
352
|
+
def getdecimal(self, key: str, *, required: t.Literal[True]) -> Decimal:
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
def getdecimal(
|
|
356
|
+
self, key: str, default: T1 = _marker, required: bool = False
|
|
357
|
+
) -> t.Union[Decimal, T1, None]:
|
|
358
|
+
"""
|
|
359
|
+
Return the named key, converted to a decimal.Decimal
|
|
286
360
|
"""
|
|
287
|
-
|
|
361
|
+
if required:
|
|
362
|
+
return self.get(key, type=Decimal, required=True)
|
|
363
|
+
else:
|
|
364
|
+
return self.get(key, default=default, type=Decimal)
|
|
365
|
+
|
|
366
|
+
@t.overload
|
|
367
|
+
def getfloat(self, key: str) -> t.Union[float, None]:
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
@t.overload
|
|
371
|
+
def getfloat(self, key: str, default: T1) -> t.Union[float, T1, None]:
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
@t.overload
|
|
375
|
+
def getfloat(self, key: str, *, required: t.Literal[True]) -> float:
|
|
376
|
+
...
|
|
377
|
+
|
|
378
|
+
def getfloat(
|
|
379
|
+
self, key: str, default: T1 = _marker, required: bool = False
|
|
380
|
+
) -> t.Union[float, T1, None]:
|
|
381
|
+
"""
|
|
382
|
+
Return the named key, converted to a float.
|
|
383
|
+
"""
|
|
384
|
+
if required:
|
|
385
|
+
return self.get(key, type=float, required=True)
|
|
386
|
+
else:
|
|
387
|
+
return self.get(key, default=default, type=float)
|
|
388
|
+
|
|
389
|
+
@t.overload
|
|
390
|
+
def getint(self, key: str) -> t.Union[int, None]:
|
|
391
|
+
...
|
|
392
|
+
|
|
393
|
+
@t.overload
|
|
394
|
+
def getint(self, key: str, default: T1) -> t.Union[int, T1, None]:
|
|
395
|
+
...
|
|
396
|
+
|
|
397
|
+
@t.overload
|
|
398
|
+
def getint(self, key: str, *, required: t.Literal[True]) -> int:
|
|
399
|
+
...
|
|
400
|
+
|
|
401
|
+
def getint(
|
|
402
|
+
self, key: str, default: T1 = _marker, required: bool = False
|
|
403
|
+
) -> t.Union[int, T1, None]:
|
|
404
|
+
"""
|
|
405
|
+
Return the named key, converted to an integer.
|
|
406
|
+
"""
|
|
407
|
+
if required:
|
|
408
|
+
return self.get(key, type=int, required=True)
|
|
409
|
+
else:
|
|
410
|
+
return self.get(key, default=default, type=int)
|
|
288
411
|
|
|
289
412
|
def __contains__(self, key):
|
|
290
413
|
"""
|
|
@@ -293,9 +416,7 @@ class Request(object):
|
|
|
293
416
|
return key in self.form
|
|
294
417
|
|
|
295
418
|
@property
|
|
296
|
-
def now( # type: ignore
|
|
297
|
-
self, now=datetime.datetime.now, utc=datetime.timezone.utc
|
|
298
|
-
):
|
|
419
|
+
def now(self, now=datetime.datetime.now, utc=datetime.timezone.utc): # type: ignore
|
|
299
420
|
"""
|
|
300
421
|
Return a timezone-aware UTC datetime instance. The value returned is
|
|
301
422
|
guaranteed to be constant throughout the lifetime of the request.
|
|
@@ -451,7 +572,7 @@ class Request(object):
|
|
|
451
572
|
return self._parsed_url
|
|
452
573
|
|
|
453
574
|
@property
|
|
454
|
-
def path_info(self, environ_to_str=environ_to_str) -> str:
|
|
575
|
+
def path_info(self, environ_to_str=environ_to_str) -> str:
|
|
455
576
|
"""
|
|
456
577
|
The PATH_INFO value as a string
|
|
457
578
|
|
|
@@ -463,9 +584,9 @@ class Request(object):
|
|
|
463
584
|
raise exceptions.BadRequest
|
|
464
585
|
|
|
465
586
|
@property
|
|
466
|
-
def script_name(self):
|
|
467
|
-
"""
|
|
468
|
-
The SCRIPT_NAME value as a
|
|
587
|
+
def script_name(self) -> str:
|
|
588
|
+
"""
|
|
589
|
+
The SCRIPT_NAME value as a string
|
|
469
590
|
|
|
470
591
|
Note that SCRIPT_NAME is already unquoted by the server
|
|
471
592
|
"""
|
|
@@ -477,12 +598,12 @@ class Request(object):
|
|
|
477
598
|
@property
|
|
478
599
|
def query_string(self) -> t.Optional[str]:
|
|
479
600
|
"""
|
|
480
|
-
The QUERY_STRING value as a
|
|
601
|
+
The QUERY_STRING value as a string
|
|
481
602
|
"""
|
|
482
603
|
return self.environ.get("QUERY_STRING")
|
|
483
604
|
|
|
484
605
|
@property
|
|
485
|
-
def referrer(self):
|
|
606
|
+
def referrer(self) -> t.Optional[str]:
|
|
486
607
|
"""
|
|
487
608
|
Return the HTTP referer header, or ``None`` if this is not available.
|
|
488
609
|
"""
|
fresco/requestcontext.py
CHANGED
|
@@ -68,6 +68,9 @@ class RequestContext(object):
|
|
|
68
68
|
def currentcontext(self) -> ContextDict:
|
|
69
69
|
return self._contexts[self._ident_func()][-1]
|
|
70
70
|
|
|
71
|
+
def __contains__(self, item):
|
|
72
|
+
return item in self._contexts[self._ident_func()][-1]
|
|
73
|
+
|
|
71
74
|
def __getattr__(self, item):
|
|
72
75
|
try:
|
|
73
76
|
return self._contexts[self._ident_func()][-1][item]
|
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
|
|
|
@@ -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.
|