fresco 3.3.4__py3-none-any.whl → 3.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fresco/__init__.py +55 -56
- fresco/core.py +39 -27
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +188 -70
- fresco/py.typed +0 -0
- fresco/request.py +157 -36
- fresco/requestcontext.py +3 -0
- fresco/response.py +66 -57
- fresco/routeargs.py +23 -9
- fresco/routing.py +152 -74
- fresco/static.py +1 -1
- fresco/subrequests.py +3 -5
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +59 -15
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_response.py +16 -0
- fresco/tests/test_routing.py +113 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/tests/util/test_urls.py +20 -0
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +44 -12
- fresco/util/wsgi.py +15 -14
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/METADATA +4 -4
- fresco-3.9.0.dist-info/RECORD +58 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.3.4.dist-info/RECORD +0 -57
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/top_level.txt +0 -0
fresco/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,50 +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: ([], ts[1])
|
|
221
|
-
if len(ts[0]) == 0
|
|
222
|
-
else (sorted(tags.index(t) for t in ts[0]), ts[1])
|
|
223
|
-
)
|
|
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=}"
|
|
224
255
|
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
|
|
257
|
+
matched = [
|
|
258
|
+
ts.path for ts in sorted(tagged_sources, key=tagged_source_sort_key(tags))
|
|
259
|
+
]
|
|
228
260
|
|
|
229
261
|
for path in matched:
|
|
230
262
|
existing_keys = set(self.keys())
|
|
@@ -232,7 +264,7 @@ class Options(dict):
|
|
|
232
264
|
if path.suffix == ".py":
|
|
233
265
|
self.update_from_file(str(path))
|
|
234
266
|
elif path.suffix == ".toml":
|
|
235
|
-
import toml
|
|
267
|
+
import toml # type: ignore
|
|
236
268
|
|
|
237
269
|
with path.open("r") as f:
|
|
238
270
|
self.update(toml.load(f))
|
|
@@ -253,16 +285,20 @@ class Options(dict):
|
|
|
253
285
|
)
|
|
254
286
|
)
|
|
255
287
|
|
|
256
|
-
if
|
|
257
|
-
|
|
288
|
+
if existing_keys and set(self.keys()) != existing_keys:
|
|
289
|
+
error_msg = (
|
|
258
290
|
f"settings file {path} created undefined options: "
|
|
259
291
|
f"{set(self.keys()) - existing_keys}"
|
|
260
292
|
)
|
|
293
|
+
if strict:
|
|
294
|
+
raise AssertionError(error_msg)
|
|
295
|
+
else:
|
|
296
|
+
logger.warning(error_msg)
|
|
261
297
|
|
|
262
298
|
if use_environ:
|
|
263
|
-
|
|
264
|
-
if k in os.environ
|
|
265
|
-
|
|
299
|
+
self |= {
|
|
300
|
+
k: parse_value(self, os.environ[k]) for k in self if k in os.environ
|
|
301
|
+
}
|
|
266
302
|
|
|
267
303
|
if trigger_onload:
|
|
268
304
|
self.do_loaded_callbacks()
|
|
@@ -278,9 +314,11 @@ class Options(dict):
|
|
|
278
314
|
:param load_all: If true private symbols will also be loaded into the
|
|
279
315
|
options object.
|
|
280
316
|
"""
|
|
281
|
-
ns: Dict[str, Any] = {"__file__": path}
|
|
317
|
+
ns: Dict[str, Any] = {"__file__": path, "options": self}
|
|
282
318
|
with open(path) as f:
|
|
283
319
|
exec(f.read(), ns)
|
|
320
|
+
if ns.get("options") is self:
|
|
321
|
+
del ns["options"]
|
|
284
322
|
self.update_from_dict(ns, load_all)
|
|
285
323
|
|
|
286
324
|
def update_from_dict(self, d, load_all=False):
|
|
@@ -314,6 +352,42 @@ class Options(dict):
|
|
|
314
352
|
self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
|
|
315
353
|
|
|
316
354
|
|
|
355
|
+
@contextlib.contextmanager
|
|
356
|
+
def override_options(options, other=None, **kwargs):
|
|
357
|
+
"""
|
|
358
|
+
Context manager that updates the given Options object with new values.
|
|
359
|
+
On exit, the old values will be restored.
|
|
360
|
+
|
|
361
|
+
This function is provided to assist with writing tests. It directly
|
|
362
|
+
modifies the given options object and does not prevent other threads from
|
|
363
|
+
accessing the modified values.
|
|
364
|
+
"""
|
|
365
|
+
saved: list[tuple[str, t.Any]] = []
|
|
366
|
+
items: t.Iterable[tuple[str, t.Any]] = []
|
|
367
|
+
if isinstance(other, Mapping):
|
|
368
|
+
items = ((k, other[k]) for k in other.keys())
|
|
369
|
+
|
|
370
|
+
if kwargs:
|
|
371
|
+
items = itertools.chain(items, kwargs.items())
|
|
372
|
+
|
|
373
|
+
NOT_PRESENT = object()
|
|
374
|
+
|
|
375
|
+
for k, v in items:
|
|
376
|
+
if k in options:
|
|
377
|
+
saved.append((k, options[k]))
|
|
378
|
+
else:
|
|
379
|
+
saved.append((k, NOT_PRESENT))
|
|
380
|
+
|
|
381
|
+
options[k] = v
|
|
382
|
+
|
|
383
|
+
yield options
|
|
384
|
+
for k, v in saved:
|
|
385
|
+
if v is NOT_PRESENT:
|
|
386
|
+
del options[k]
|
|
387
|
+
else:
|
|
388
|
+
options[k] = v
|
|
389
|
+
|
|
390
|
+
|
|
317
391
|
def parse_value(
|
|
318
392
|
options: Mapping,
|
|
319
393
|
v: str,
|
|
@@ -416,3 +490,47 @@ def dict_from_options(
|
|
|
416
490
|
if recursive:
|
|
417
491
|
return recursive_split(d, prefix[-1])
|
|
418
492
|
return d
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@dataclasses.dataclass
|
|
496
|
+
class TaggedSource:
|
|
497
|
+
priority: int
|
|
498
|
+
tags: list[str]
|
|
499
|
+
path: Path
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def tagged_source_sort_key(
|
|
503
|
+
tags: list[str],
|
|
504
|
+
) -> Callable[[TaggedSource], tuple[int, list[int], str]]:
|
|
505
|
+
"""
|
|
506
|
+
Return a function that can be used as the ``key`` argument of``sorted``,
|
|
507
|
+
that sorts TaggedSource objects based on
|
|
508
|
+
priority, tag order, then filename.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
def _sort_key(ts: TaggedSource) -> tuple[int, list[int], str]:
|
|
512
|
+
sorted_tags = [tags.index(t) for t in ts.tags]
|
|
513
|
+
return (ts.priority, sorted_tags, str(ts.path))
|
|
514
|
+
|
|
515
|
+
return _sort_key
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def make_tag_substitutions():
|
|
519
|
+
"""
|
|
520
|
+
Return a dict of substitutions to make in tag names, based on the current
|
|
521
|
+
hostname and environment variables
|
|
522
|
+
|
|
523
|
+
This permits tags to be specified as, for example,
|
|
524
|
+
'{hostname}' or '{FRESCO_PROFILE}'
|
|
525
|
+
and the associated value will be substituted in.
|
|
526
|
+
|
|
527
|
+
Path separators and dots (which separate tags) are replaced in substitution
|
|
528
|
+
values by an underscore.
|
|
529
|
+
"""
|
|
530
|
+
substitutions = os.environ.copy()
|
|
531
|
+
substitutions["hostname"] = gethostname()
|
|
532
|
+
substitutions = {
|
|
533
|
+
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
534
|
+
for k, v in substitutions.items()
|
|
535
|
+
}
|
|
536
|
+
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
|
|
@@ -185,7 +195,7 @@ class Request(object):
|
|
|
185
195
|
"Payload contains data that could not be decoded using " + encoding
|
|
186
196
|
)
|
|
187
197
|
|
|
188
|
-
def get_json(self, *args, **kwargs):
|
|
198
|
+
def get_json(self, *args, **kwargs) -> t.Any:
|
|
189
199
|
"""
|
|
190
200
|
Return a decoded JSON request body.
|
|
191
201
|
|
|
@@ -196,12 +206,14 @@ 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
|
|
|
203
215
|
@property
|
|
204
|
-
def files(self):
|
|
216
|
+
def files(self) -> MultiDict:
|
|
205
217
|
"""
|
|
206
218
|
Return a MultiDict of all ``FileUpload`` objects available.
|
|
207
219
|
"""
|
|
@@ -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] = str) -> 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]:
|
|
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]
|