fresco 3.4.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 +36 -26
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +182 -70
- fresco/py.typed +0 -0
- fresco/request.py +155 -34
- fresco/requestcontext.py +3 -0
- fresco/response.py +12 -9
- fresco/routeargs.py +23 -9
- fresco/routing.py +133 -71
- 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_routing.py +86 -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.4.0.dist-info → fresco-3.6.0.dist-info}/METADATA +3 -2
- fresco-3.6.0.dist-info/RECORD +58 -0
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.4.0.dist-info/RECORD +0 -57
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info}/top_level.txt +0 -0
fresco/options.py
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
#
|
|
15
|
+
import contextlib
|
|
16
|
+
import dataclasses
|
|
15
17
|
import inspect
|
|
18
|
+
import itertools
|
|
16
19
|
import json
|
|
17
20
|
import logging
|
|
18
21
|
import typing as t
|
|
19
22
|
import os
|
|
20
23
|
import re
|
|
24
|
+
from collections.abc import Mapping
|
|
21
25
|
from decimal import Decimal
|
|
22
26
|
from pathlib import Path
|
|
23
27
|
from socket import gethostname
|
|
@@ -26,7 +30,6 @@ from typing import Callable
|
|
|
26
30
|
from typing import Dict
|
|
27
31
|
from typing import Iterable
|
|
28
32
|
from typing import List
|
|
29
|
-
from typing import Mapping
|
|
30
33
|
from typing import Sequence
|
|
31
34
|
from typing import Union
|
|
32
35
|
|
|
@@ -35,6 +38,8 @@ from fresco.exceptions import OptionsLoadedException
|
|
|
35
38
|
__all__ = ["Options"]
|
|
36
39
|
|
|
37
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]+)(?:-(.*))?$")
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
class Options(dict):
|
|
@@ -96,7 +101,7 @@ class Options(dict):
|
|
|
96
101
|
strict=True,
|
|
97
102
|
dir=None,
|
|
98
103
|
trigger_onload=True,
|
|
99
|
-
):
|
|
104
|
+
) -> "Options":
|
|
100
105
|
"""
|
|
101
106
|
Find all files matching glob pattern ``sources`` and populates the
|
|
102
107
|
options object from those with matching filenames containing ``tags``.
|
|
@@ -119,20 +124,43 @@ class Options(dict):
|
|
|
119
124
|
format. Any other files will be interpreted as simple lists of
|
|
120
125
|
```key=value`` pairs.
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
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``.
|
|
124
158
|
The filename ``setttings.dev.local.ini`` would be
|
|
125
159
|
considered to have the tags ``('dev', 'local')``
|
|
126
160
|
|
|
127
161
|
Where filename contain multiple tags, all tags must match for the file
|
|
128
162
|
to be loaded.
|
|
129
163
|
|
|
130
|
-
Files are processed in the order that tags are specified in the
|
|
131
|
-
``tags`` parameter, and then in lexicographical order.
|
|
132
|
-
For example, calling ``options.load(..., tags=["dev", "local"])`` would
|
|
133
|
-
cause a file named "settings.dev" to be loaded before one named
|
|
134
|
-
"settings.local".
|
|
135
|
-
|
|
136
164
|
Tag names may contain the names of environment variable surrounded by
|
|
137
165
|
braces, for example ``{USER}``. These will be substituted for the
|
|
138
166
|
environment variable's value, with any dots or path separators replaced
|
|
@@ -143,8 +171,24 @@ class Options(dict):
|
|
|
143
171
|
|
|
144
172
|
Files with the suffix ".sample" are unconditionally excluded.
|
|
145
173
|
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
192
|
|
|
149
193
|
Example::
|
|
150
194
|
|
|
@@ -152,19 +196,12 @@ class Options(dict):
|
|
|
152
196
|
opts.load(".env*", ["dev", "host-{hostname}", "local"])
|
|
153
197
|
|
|
154
198
|
Would load options from files named ``.env``, ``.env.json``, ``.env.dev.py``
|
|
155
|
-
and ``.env.local.py
|
|
199
|
+
and ``.env.local.py``, in that order.
|
|
156
200
|
|
|
157
201
|
"""
|
|
158
202
|
if self._is_loaded:
|
|
159
203
|
raise OptionsLoadedException("Options have already been loaded")
|
|
160
204
|
|
|
161
|
-
tag_substitutions = os.environ.copy()
|
|
162
|
-
tag_substitutions["hostname"] = gethostname()
|
|
163
|
-
tag_substitutions = {
|
|
164
|
-
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
165
|
-
for k, v in tag_substitutions.items()
|
|
166
|
-
}
|
|
167
|
-
|
|
168
205
|
candidates: List[Path] = []
|
|
169
206
|
if dir is None:
|
|
170
207
|
dir = Path(".")
|
|
@@ -181,52 +218,45 @@ class Options(dict):
|
|
|
181
218
|
if p.suffix.lower() != ".sample"
|
|
182
219
|
)
|
|
183
220
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
tagged_sources,
|
|
219
|
-
key=(
|
|
220
|
-
lambda ts: (
|
|
221
|
-
([], ts[1])
|
|
222
|
-
if len(ts[0]) == 0 else
|
|
223
|
-
(sorted(tags.index(t) for t in ts[0]), ts[1])
|
|
224
|
-
)
|
|
225
|
-
)
|
|
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=}"
|
|
226
255
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
|
|
257
|
+
matched = [
|
|
258
|
+
ts.path for ts in sorted(tagged_sources, key=tagged_source_sort_key(tags))
|
|
259
|
+
]
|
|
230
260
|
|
|
231
261
|
for path in matched:
|
|
232
262
|
existing_keys = set(self.keys())
|
|
@@ -234,7 +264,7 @@ class Options(dict):
|
|
|
234
264
|
if path.suffix == ".py":
|
|
235
265
|
self.update_from_file(str(path))
|
|
236
266
|
elif path.suffix == ".toml":
|
|
237
|
-
import toml
|
|
267
|
+
import toml # type: ignore
|
|
238
268
|
|
|
239
269
|
with path.open("r") as f:
|
|
240
270
|
self.update(toml.load(f))
|
|
@@ -262,9 +292,9 @@ class Options(dict):
|
|
|
262
292
|
)
|
|
263
293
|
|
|
264
294
|
if use_environ:
|
|
265
|
-
|
|
266
|
-
if k in os.environ
|
|
267
|
-
|
|
295
|
+
self |= {
|
|
296
|
+
k: parse_value(self, os.environ[k]) for k in self if k in os.environ
|
|
297
|
+
}
|
|
268
298
|
|
|
269
299
|
if trigger_onload:
|
|
270
300
|
self.do_loaded_callbacks()
|
|
@@ -280,9 +310,11 @@ class Options(dict):
|
|
|
280
310
|
:param load_all: If true private symbols will also be loaded into the
|
|
281
311
|
options object.
|
|
282
312
|
"""
|
|
283
|
-
ns: Dict[str, Any] = {"__file__": path}
|
|
313
|
+
ns: Dict[str, Any] = {"__file__": path, "options": self}
|
|
284
314
|
with open(path) as f:
|
|
285
315
|
exec(f.read(), ns)
|
|
316
|
+
if ns.get("options") is self:
|
|
317
|
+
del ns["options"]
|
|
286
318
|
self.update_from_dict(ns, load_all)
|
|
287
319
|
|
|
288
320
|
def update_from_dict(self, d, load_all=False):
|
|
@@ -316,6 +348,42 @@ class Options(dict):
|
|
|
316
348
|
self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
|
|
317
349
|
|
|
318
350
|
|
|
351
|
+
@contextlib.contextmanager
|
|
352
|
+
def override_options(options, other=None, **kwargs):
|
|
353
|
+
"""
|
|
354
|
+
Context manager that updates the given Options object with new values.
|
|
355
|
+
On exit, the old values will be restored.
|
|
356
|
+
|
|
357
|
+
This function is provided to assist with writing tests. It directly
|
|
358
|
+
modifies the given options object and does not prevent other threads from
|
|
359
|
+
accessing the modified values.
|
|
360
|
+
"""
|
|
361
|
+
saved: list[tuple[str, t.Any]] = []
|
|
362
|
+
items: t.Iterable[tuple[str, t.Any]] = []
|
|
363
|
+
if isinstance(other, Mapping):
|
|
364
|
+
items = ((k, other[k]) for k in other.keys())
|
|
365
|
+
|
|
366
|
+
if kwargs:
|
|
367
|
+
items = itertools.chain(items, kwargs.items())
|
|
368
|
+
|
|
369
|
+
NOT_PRESENT = object()
|
|
370
|
+
|
|
371
|
+
for k, v in items:
|
|
372
|
+
if k in options:
|
|
373
|
+
saved.append((k, options[k]))
|
|
374
|
+
else:
|
|
375
|
+
saved.append((k, NOT_PRESENT))
|
|
376
|
+
|
|
377
|
+
options[k] = v
|
|
378
|
+
|
|
379
|
+
yield options
|
|
380
|
+
for k, v in saved:
|
|
381
|
+
if v is NOT_PRESENT:
|
|
382
|
+
del options[k]
|
|
383
|
+
else:
|
|
384
|
+
options[k] = v
|
|
385
|
+
|
|
386
|
+
|
|
319
387
|
def parse_value(
|
|
320
388
|
options: Mapping,
|
|
321
389
|
v: str,
|
|
@@ -418,3 +486,47 @@ def dict_from_options(
|
|
|
418
486
|
if recursive:
|
|
419
487
|
return recursive_split(d, prefix[-1])
|
|
420
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/py.typed
ADDED
|
File without changes
|
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]
|