fresco 3.2.0__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fresco might be problematic. Click here for more details.
- {fresco-3.2.0 → fresco-3.3.1}/CHANGELOG.rst +16 -0
- {fresco-3.2.0/fresco.egg-info → fresco-3.3.1}/PKG-INFO +1 -1
- fresco-3.3.1/fresco/__init__.py +128 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/core.py +10 -8
- {fresco-3.2.0 → fresco-3.3.1}/fresco/middleware.py +1 -1
- {fresco-3.2.0 → fresco-3.3.1}/fresco/multidict.py +51 -34
- {fresco-3.2.0 → fresco-3.3.1}/fresco/options.py +131 -21
- {fresco-3.2.0 → fresco-3.3.1}/fresco/request.py +1 -1
- {fresco-3.2.0 → fresco-3.3.1}/fresco/requestcontext.py +16 -9
- {fresco-3.2.0 → fresco-3.3.1}/fresco/response.py +24 -23
- {fresco-3.2.0 → fresco-3.3.1}/fresco/routing.py +41 -10
- {fresco-3.2.0 → fresco-3.3.1}/fresco/subrequests.py +3 -2
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_options.py +85 -5
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_routing.py +14 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/http.py +2 -1
- {fresco-3.2.0 → fresco-3.3.1/fresco.egg-info}/PKG-INFO +1 -1
- fresco-3.2.0/fresco/__init__.py +0 -27
- {fresco-3.2.0 → fresco-3.3.1}/LICENSE.txt +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/MANIFEST.in +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/README.rst +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/cookie.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/decorators.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/exceptions.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/routeargs.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/static.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/__init__.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/fixtures.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_cookie.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_core.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_decorators.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_exceptions.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_middleware.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_multidict.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_request.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_requestcontext.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_response.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_routeargs.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_static.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/test_subrequests.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/__init__.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/form_data.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/test_common.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/test_http.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/test_security.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/test_urls.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/tests/util/test_wsgi.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/types.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/typing.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/__init__.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/cache.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/common.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/contentencodings.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/file.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/io.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/object.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/security.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/textproc.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/urls.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco/util/wsgi.py +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco.egg-info/SOURCES.txt +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco.egg-info/dependency_links.txt +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/fresco.egg-info/top_level.txt +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/setup.cfg +0 -0
- {fresco-3.2.0 → fresco-3.3.1}/setup.py +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
3.3.1 (released 2023-05-18)
|
|
5
|
+
---------------------------
|
|
6
|
+
|
|
7
|
+
- The ``route_wsgi`` and ``route_all`` methods now only match at a
|
|
8
|
+
path separator boundary (``'/'``).
|
|
9
|
+
- Bugfix: subrequest now works with views routed via ``RRoute`` (and so expect
|
|
10
|
+
an initial ``request`` argument)
|
|
11
|
+
|
|
12
|
+
3.3.0
|
|
13
|
+
-----
|
|
14
|
+
|
|
15
|
+
- Options: fresco.options.Options can now take a list of paths as its first argument
|
|
16
|
+
- Options: tags can now be specified with environment variable substitutions
|
|
17
|
+
(eg "{FOO}" would load files tagged with the current value of the 'FOO'
|
|
18
|
+
environment variable)
|
|
19
|
+
- Options: add ``fresco.options.list_from_str`` and ``fresco.options.dict_from_options``
|
|
4
20
|
|
|
5
21
|
3.2.0 (released 2023-04-16)
|
|
6
22
|
---------------------------
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright 2015 Oliver Cope
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
__version__ = "3.3.1"
|
|
16
|
+
|
|
17
|
+
DEFAULT_CHARSET = "UTF-8"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Request",
|
|
21
|
+
"currentrequest",
|
|
22
|
+
"context",
|
|
23
|
+
"Response",
|
|
24
|
+
"FrescoApp",
|
|
25
|
+
"urlfor",
|
|
26
|
+
"ALL_METHODS",
|
|
27
|
+
"DEFAULT_CHARSET",
|
|
28
|
+
"GET",
|
|
29
|
+
"HEAD",
|
|
30
|
+
"POST",
|
|
31
|
+
"PUT",
|
|
32
|
+
"DELETE",
|
|
33
|
+
"OPTIONS",
|
|
34
|
+
"TRACE",
|
|
35
|
+
"CONNECT",
|
|
36
|
+
"VERSION_CONTROL",
|
|
37
|
+
"REPORT",
|
|
38
|
+
"CHECKOUT",
|
|
39
|
+
"CHECKIN",
|
|
40
|
+
"UNCHECKOUT",
|
|
41
|
+
"MKWORKSPACE",
|
|
42
|
+
"UPDATE",
|
|
43
|
+
"LABEL",
|
|
44
|
+
"MERGE",
|
|
45
|
+
"BASELINE_CONTROL",
|
|
46
|
+
"MKACTIVITY",
|
|
47
|
+
"ORDERPATCH",
|
|
48
|
+
"ACL",
|
|
49
|
+
"SEARCH",
|
|
50
|
+
"PATCH",
|
|
51
|
+
"DelegateRoute",
|
|
52
|
+
"Options",
|
|
53
|
+
"Route",
|
|
54
|
+
"RouteCollection",
|
|
55
|
+
"RRoute",
|
|
56
|
+
"routearg",
|
|
57
|
+
"FormArg",
|
|
58
|
+
"PostArg",
|
|
59
|
+
"QueryArg",
|
|
60
|
+
"GetArg",
|
|
61
|
+
"CookieArg",
|
|
62
|
+
"SessionArg",
|
|
63
|
+
"RequestObject",
|
|
64
|
+
"FormData",
|
|
65
|
+
"PostData",
|
|
66
|
+
"QueryData",
|
|
67
|
+
"GetData",
|
|
68
|
+
"routefor",
|
|
69
|
+
"XForwarded",
|
|
70
|
+
"subrequest",
|
|
71
|
+
"subrequest_bytes",
|
|
72
|
+
"subrequest_raw",
|
|
73
|
+
"object_or_404",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
from fresco.request import Request
|
|
77
|
+
from fresco.request import currentrequest
|
|
78
|
+
from fresco.requestcontext import context
|
|
79
|
+
from fresco.response import Response
|
|
80
|
+
from fresco.core import FrescoApp
|
|
81
|
+
from fresco.core import urlfor
|
|
82
|
+
from fresco.options import Options
|
|
83
|
+
from fresco.routing import Route
|
|
84
|
+
from fresco.routing import RRoute
|
|
85
|
+
from fresco.routing import RouteCollection
|
|
86
|
+
from fresco.routing import DelegateRoute
|
|
87
|
+
from fresco.routing import routefor
|
|
88
|
+
from fresco.routing import ALL_METHODS
|
|
89
|
+
from fresco.routing import GET
|
|
90
|
+
from fresco.routing import HEAD
|
|
91
|
+
from fresco.routing import POST
|
|
92
|
+
from fresco.routing import PUT
|
|
93
|
+
from fresco.routing import DELETE
|
|
94
|
+
from fresco.routing import OPTIONS
|
|
95
|
+
from fresco.routing import TRACE
|
|
96
|
+
from fresco.routing import CONNECT
|
|
97
|
+
from fresco.routing import VERSION_CONTROL
|
|
98
|
+
from fresco.routing import REPORT
|
|
99
|
+
from fresco.routing import CHECKOUT
|
|
100
|
+
from fresco.routing import CHECKIN
|
|
101
|
+
from fresco.routing import UNCHECKOUT
|
|
102
|
+
from fresco.routing import MKWORKSPACE
|
|
103
|
+
from fresco.routing import UPDATE
|
|
104
|
+
from fresco.routing import LABEL
|
|
105
|
+
from fresco.routing import MERGE
|
|
106
|
+
from fresco.routing import BASELINE_CONTROL
|
|
107
|
+
from fresco.routing import MKACTIVITY
|
|
108
|
+
from fresco.routing import ORDERPATCH
|
|
109
|
+
from fresco.routing import ACL
|
|
110
|
+
from fresco.routing import SEARCH
|
|
111
|
+
from fresco.routing import PATCH
|
|
112
|
+
from fresco.routeargs import routearg
|
|
113
|
+
from fresco.routeargs import FormArg
|
|
114
|
+
from fresco.routeargs import PostArg
|
|
115
|
+
from fresco.routeargs import QueryArg
|
|
116
|
+
from fresco.routeargs import GetArg
|
|
117
|
+
from fresco.routeargs import CookieArg
|
|
118
|
+
from fresco.routeargs import SessionArg
|
|
119
|
+
from fresco.routeargs import RequestObject
|
|
120
|
+
from fresco.routeargs import FormData
|
|
121
|
+
from fresco.routeargs import PostData
|
|
122
|
+
from fresco.routeargs import QueryData
|
|
123
|
+
from fresco.routeargs import GetData
|
|
124
|
+
from fresco.middleware import XForwarded
|
|
125
|
+
from fresco.subrequests import subrequest
|
|
126
|
+
from fresco.subrequests import subrequest_bytes
|
|
127
|
+
from fresco.subrequests import subrequest_raw
|
|
128
|
+
from fresco.util.common import object_or_404
|
|
@@ -151,9 +151,9 @@ class FrescoApp(RouteCollection):
|
|
|
151
151
|
|
|
152
152
|
def get_response(
|
|
153
153
|
self,
|
|
154
|
-
request,
|
|
155
|
-
path,
|
|
156
|
-
method,
|
|
154
|
+
request: Request,
|
|
155
|
+
path: str,
|
|
156
|
+
method: str,
|
|
157
157
|
currentcontext=context.currentcontext,
|
|
158
158
|
normpath=normpath,
|
|
159
159
|
):
|
|
@@ -259,12 +259,12 @@ class FrescoApp(RouteCollection):
|
|
|
259
259
|
|
|
260
260
|
# Is the URL just missing a trailing '/'?
|
|
261
261
|
if not path or path[-1] != "/":
|
|
262
|
-
|
|
262
|
+
if self.get_methods(request, path + "/"):
|
|
263
263
|
return Response.unrestricted_redirect_permanent(path + "/")
|
|
264
264
|
|
|
265
265
|
return Response.not_found()
|
|
266
266
|
|
|
267
|
-
def view(self, request=None) -> Response:
|
|
267
|
+
def view(self, request: t.Optional[Request] = None) -> Response:
|
|
268
268
|
request = request or context.request
|
|
269
269
|
try:
|
|
270
270
|
path = request.path_info
|
|
@@ -287,7 +287,9 @@ class FrescoApp(RouteCollection):
|
|
|
287
287
|
|
|
288
288
|
return response
|
|
289
289
|
|
|
290
|
-
def handle_http_error_response(
|
|
290
|
+
def handle_http_error_response(
|
|
291
|
+
self, request: Request, response: Response
|
|
292
|
+
) -> Response:
|
|
291
293
|
"""
|
|
292
294
|
Call any process_http_error_response handlers and return the
|
|
293
295
|
(potentially modified) response object.
|
|
@@ -303,8 +305,8 @@ class FrescoApp(RouteCollection):
|
|
|
303
305
|
self.log_exception(request)
|
|
304
306
|
return response
|
|
305
307
|
|
|
306
|
-
def get_methods(self, request, path):
|
|
307
|
-
"""
|
|
308
|
+
def get_methods(self, request: Request, path: str) -> Set[str]:
|
|
309
|
+
"""
|
|
308
310
|
Return the HTTP methods valid in routes to the given path
|
|
309
311
|
"""
|
|
310
312
|
methods: Set[str] = set()
|
|
@@ -16,12 +16,24 @@
|
|
|
16
16
|
An order preserving multidict implementation
|
|
17
17
|
"""
|
|
18
18
|
from collections.abc import MutableMapping
|
|
19
|
+
from collections.abc import Mapping
|
|
19
20
|
from itertools import repeat
|
|
21
|
+
import typing as t
|
|
20
22
|
from typing import Any
|
|
21
23
|
from typing import Dict
|
|
22
24
|
from typing import Set
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
if t.TYPE_CHECKING:
|
|
28
|
+
from typeshed import SupportsKeysAndGetItem
|
|
29
|
+
else:
|
|
30
|
+
SupportsKeysAndGetItem = Mapping
|
|
31
|
+
|
|
32
|
+
SupportsKeysAndGetItemOrIterable = t.Union[
|
|
33
|
+
SupportsKeysAndGetItem, t.Iterable[t.Tuple[Any, Any]]
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
25
37
|
class MultiDict(MutableMapping):
|
|
26
38
|
"""
|
|
27
39
|
Dictionary-like object that supports multiple values per key. Insertion
|
|
@@ -44,9 +56,13 @@ class MultiDict(MutableMapping):
|
|
|
44
56
|
|
|
45
57
|
"""
|
|
46
58
|
|
|
59
|
+
_dict: Dict[Any, Any]
|
|
60
|
+
_order: t.List[Any]
|
|
47
61
|
setdefault = MutableMapping.setdefault
|
|
48
62
|
|
|
49
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self, mapping_or_iterable: SupportsKeysAndGetItemOrIterable = tuple(), **kwargs
|
|
65
|
+
):
|
|
50
66
|
"""
|
|
51
67
|
MultiDicts can be constructed in the following ways:
|
|
52
68
|
|
|
@@ -76,13 +92,9 @@ class MultiDict(MutableMapping):
|
|
|
76
92
|
MultiDict(...)
|
|
77
93
|
|
|
78
94
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
% (self.__class__.__name__, 1 + len(args))
|
|
83
|
-
)
|
|
84
|
-
self.clear()
|
|
85
|
-
self.update(*args, **kwargs)
|
|
95
|
+
self._order = []
|
|
96
|
+
self._dict = {}
|
|
97
|
+
self._update(mapping_or_iterable, True, kwargs)
|
|
86
98
|
|
|
87
99
|
def __getitem__(self, key):
|
|
88
100
|
"""
|
|
@@ -289,7 +301,7 @@ class MultiDict(MutableMapping):
|
|
|
289
301
|
self._order.remove((key, value))
|
|
290
302
|
return value
|
|
291
303
|
|
|
292
|
-
def popitem(self):
|
|
304
|
+
def popitem(self) -> t.Tuple[Any, Any]:
|
|
293
305
|
"""
|
|
294
306
|
Return and remove a ``(key, value)`` pair from the dictionary.
|
|
295
307
|
|
|
@@ -314,7 +326,17 @@ class MultiDict(MutableMapping):
|
|
|
314
326
|
raise KeyError("popitem(): dictionary is empty")
|
|
315
327
|
return key, self.pop(key)
|
|
316
328
|
|
|
317
|
-
|
|
329
|
+
@t.overload
|
|
330
|
+
def update(
|
|
331
|
+
self, mapping_or_iterable: SupportsKeysAndGetItem, /, **kwargs: Any
|
|
332
|
+
) -> None:
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
@t.overload
|
|
336
|
+
def update(self, /, **kwargs: Any) -> None:
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
def update(self, mapping_or_iterable=tuple(), **kwargs):
|
|
318
340
|
r"""
|
|
319
341
|
Update the MultiDict from another MultiDict, regular dictionary or a
|
|
320
342
|
iterable of ``(key, value)`` pairs. New keys overwrite old keys - use
|
|
@@ -349,29 +371,24 @@ class MultiDict(MutableMapping):
|
|
|
349
371
|
>>> d.update(mood='okay')
|
|
350
372
|
|
|
351
373
|
"""
|
|
352
|
-
|
|
353
|
-
raise TypeError("expected at most 1 argument, got %d" % (1 + len(args),))
|
|
354
|
-
if args:
|
|
355
|
-
other = args[0]
|
|
356
|
-
else:
|
|
357
|
-
other = []
|
|
358
|
-
return self._update(other, True, **kwargs)
|
|
374
|
+
self._update(mapping_or_iterable, True, kwargs)
|
|
359
375
|
|
|
360
|
-
def extend(
|
|
376
|
+
def extend(
|
|
377
|
+
self, mapping_or_iterable: SupportsKeysAndGetItemOrIterable = tuple(), **kwargs
|
|
378
|
+
):
|
|
361
379
|
"""
|
|
362
380
|
Extend the MultiDict with another MultiDict, regular dictionary or a
|
|
363
381
|
iterable of ``(key, value)`` pairs. This is similar to :meth:`update`
|
|
364
382
|
except that new keys are added to old keys.
|
|
365
383
|
"""
|
|
366
|
-
|
|
367
|
-
raise TypeError("expected at most 1 argument, got %d", (1 + len(args),))
|
|
368
|
-
if args:
|
|
369
|
-
other = args[0]
|
|
370
|
-
else:
|
|
371
|
-
other = []
|
|
372
|
-
return self._update(other, False, **kwargs)
|
|
384
|
+
return self._update(mapping_or_iterable, False, kwargs)
|
|
373
385
|
|
|
374
|
-
def _update(
|
|
386
|
+
def _update(
|
|
387
|
+
self,
|
|
388
|
+
other: SupportsKeysAndGetItemOrIterable,
|
|
389
|
+
replace: bool,
|
|
390
|
+
extra_kwargs: Dict[Any, Any],
|
|
391
|
+
):
|
|
375
392
|
"""
|
|
376
393
|
Update the MultiDict from another object and optionally kwargs.
|
|
377
394
|
|
|
@@ -380,17 +397,15 @@ class MultiDict(MutableMapping):
|
|
|
380
397
|
:param replace: if ``True``, entries will replace rather than extend
|
|
381
398
|
existing entries (second positional arg)
|
|
382
399
|
"""
|
|
383
|
-
other, replace = args
|
|
384
|
-
|
|
385
400
|
if isinstance(other, self.__class__):
|
|
386
401
|
items = list(other.allitems())
|
|
387
|
-
elif isinstance(other,
|
|
402
|
+
elif isinstance(other, Mapping):
|
|
388
403
|
items = list(other.items())
|
|
389
404
|
else:
|
|
390
405
|
items = list(other)
|
|
391
406
|
|
|
392
|
-
if
|
|
393
|
-
items += list(
|
|
407
|
+
if extra_kwargs:
|
|
408
|
+
items += list(extra_kwargs.items())
|
|
394
409
|
|
|
395
410
|
if replace:
|
|
396
411
|
replaced = {k for k, v in items if k in self._dict}
|
|
@@ -398,9 +413,11 @@ class MultiDict(MutableMapping):
|
|
|
398
413
|
for key in replaced:
|
|
399
414
|
self._dict[key] = []
|
|
400
415
|
|
|
416
|
+
setdefault = self._dict.setdefault
|
|
417
|
+
append_order = self._order.append
|
|
401
418
|
for k, v in items:
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
setdefault(k, []).append(v)
|
|
420
|
+
append_order((k, v))
|
|
404
421
|
|
|
405
422
|
def __repr__(self):
|
|
406
423
|
"""
|
|
@@ -428,4 +445,4 @@ class MultiDict(MutableMapping):
|
|
|
428
445
|
|
|
429
446
|
def clear(self):
|
|
430
447
|
self._order = []
|
|
431
|
-
self._dict
|
|
448
|
+
self._dict = {}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import inspect
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
import typing as t
|
|
18
19
|
import os
|
|
19
20
|
import re
|
|
20
21
|
from decimal import Decimal
|
|
@@ -31,6 +32,8 @@ from typing import Union
|
|
|
31
32
|
|
|
32
33
|
from fresco.exceptions import OptionsLoadedException
|
|
33
34
|
|
|
35
|
+
__all__ = ["Options"]
|
|
36
|
+
|
|
34
37
|
logger = logging.getLogger(__name__)
|
|
35
38
|
|
|
36
39
|
|
|
@@ -72,25 +75,39 @@ class Options(dict):
|
|
|
72
75
|
for func in self._loaded_callbacks:
|
|
73
76
|
func(self)
|
|
74
77
|
|
|
78
|
+
def trigger_onload(self):
|
|
79
|
+
"""
|
|
80
|
+
Mark the options object as having loaded and call any registered
|
|
81
|
+
onload callbacks
|
|
82
|
+
"""
|
|
83
|
+
if self._is_loaded:
|
|
84
|
+
raise OptionsLoadedException("Options have already been loaded")
|
|
85
|
+
self.do_loaded_callbacks()
|
|
86
|
+
self.__dict__["_is_loaded"] = True
|
|
87
|
+
|
|
75
88
|
def copy(self):
|
|
76
89
|
return self.__class__(super().copy())
|
|
77
90
|
|
|
78
91
|
def load(
|
|
79
92
|
self,
|
|
80
|
-
sources: str,
|
|
93
|
+
sources: t.Union[str, t.Iterable[t.Union[Path, str]]],
|
|
81
94
|
tags: Sequence[str] = [],
|
|
82
95
|
use_environ=False,
|
|
83
96
|
strict=True,
|
|
84
97
|
dir=None,
|
|
98
|
+
trigger_onload=True,
|
|
85
99
|
):
|
|
86
100
|
"""
|
|
87
101
|
Find all files matching glob pattern ``sources`` and populates the
|
|
88
102
|
options object from those with matching filenames containing ``tags``.
|
|
89
103
|
|
|
90
|
-
:param sources:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
:param sources:
|
|
105
|
+
one or more glob patterns separated by ";",
|
|
106
|
+
or a list of glob patterns
|
|
107
|
+
|
|
108
|
+
:param tags:
|
|
109
|
+
a list of tags to look for in file names.
|
|
110
|
+
|
|
94
111
|
:param use_environ: if true, environment variables matching previously
|
|
95
112
|
loaded keys will be loaded into the options object.
|
|
96
113
|
This happens after all files have been processed.
|
|
@@ -102,13 +119,21 @@ class Options(dict):
|
|
|
102
119
|
format. Any other files will be interpreted as simple lists of
|
|
103
120
|
```key=value`` pairs.
|
|
104
121
|
|
|
105
|
-
Tags in filenames
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
Tags in filenames are delimited with periods,
|
|
123
|
+
for example ".env.production.py".
|
|
124
|
+
The filename ``setttings.dev.local.ini`` would be
|
|
125
|
+
considered to have the tags ``('dev', 'local')``
|
|
126
|
+
|
|
127
|
+
Where filename contain multiple tags, all tags must match for the file
|
|
128
|
+
to be loaded.
|
|
129
|
+
|
|
130
|
+
Tag names may contain the names of environment variable surrounded by
|
|
131
|
+
braces, for example ``{USER}``. These will be substituted for the
|
|
132
|
+
environment variable's value, with any dots or path separators replaced
|
|
133
|
+
by underscores.
|
|
108
134
|
|
|
109
|
-
The
|
|
110
|
-
|
|
111
|
-
underscores.
|
|
135
|
+
The special variable ``{hostname}`` will be substituted for the current
|
|
136
|
+
host's name with dots replaced by underscores.
|
|
112
137
|
|
|
113
138
|
Files with the suffix ".sample" are unconditionally excluded.
|
|
114
139
|
|
|
@@ -118,7 +143,7 @@ class Options(dict):
|
|
|
118
143
|
Example::
|
|
119
144
|
|
|
120
145
|
opts = Options()
|
|
121
|
-
opts.load(
|
|
146
|
+
opts.load(".env*", ["dev", "host-{hostname}", "local"])
|
|
122
147
|
|
|
123
148
|
Would load options from files named ``.env``, ``.env.json``, ``.env.dev.py``
|
|
124
149
|
and ``.env.local.py.``
|
|
@@ -126,15 +151,24 @@ class Options(dict):
|
|
|
126
151
|
"""
|
|
127
152
|
if self._is_loaded:
|
|
128
153
|
raise OptionsLoadedException("Options have already been loaded")
|
|
129
|
-
|
|
154
|
+
|
|
155
|
+
tag_substitutions = os.environ.copy()
|
|
156
|
+
tag_substitutions["hostname"] = gethostname()
|
|
157
|
+
tag_substitutions = {
|
|
158
|
+
k: v.replace(".", "_").replace(os.pathsep, "_")
|
|
159
|
+
for k, v in tag_substitutions.items()
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
candidates: List[Path] = []
|
|
131
163
|
if dir is None:
|
|
132
164
|
dir = Path(".")
|
|
133
165
|
else:
|
|
134
166
|
dir = Path(dir)
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
if isinstance(sources, str):
|
|
169
|
+
sources = [s.strip() for s in sources.split(";")]
|
|
170
|
+
for source in sources:
|
|
171
|
+
sourcepath = dir / Path(source)
|
|
138
172
|
candidates.extend(
|
|
139
173
|
p
|
|
140
174
|
for p in sourcepath.parent.glob(sourcepath.name)
|
|
@@ -142,7 +176,13 @@ class Options(dict):
|
|
|
142
176
|
)
|
|
143
177
|
|
|
144
178
|
if tags:
|
|
145
|
-
|
|
179
|
+
subbed_tags = []
|
|
180
|
+
for tag in tags:
|
|
181
|
+
try:
|
|
182
|
+
subbed_tags.append(tag.format(**tag_substitutions))
|
|
183
|
+
except KeyError:
|
|
184
|
+
pass
|
|
185
|
+
tags = subbed_tags
|
|
146
186
|
tagged_sources = []
|
|
147
187
|
for p in candidates:
|
|
148
188
|
candidate_tags = [t for t in str(p.name).split(".") if t][1:]
|
|
@@ -216,8 +256,9 @@ class Options(dict):
|
|
|
216
256
|
if k in os.environ:
|
|
217
257
|
self[k] = parse_value(self, os.environ[k])
|
|
218
258
|
|
|
219
|
-
|
|
220
|
-
|
|
259
|
+
if trigger_onload:
|
|
260
|
+
self.do_loaded_callbacks()
|
|
261
|
+
self.__dict__["_is_loaded"] = True
|
|
221
262
|
return self
|
|
222
263
|
|
|
223
264
|
def update_from_file(self, path, load_all=False):
|
|
@@ -256,9 +297,11 @@ class Options(dict):
|
|
|
256
297
|
|
|
257
298
|
def update_from_object(self, ob, load_all=False):
|
|
258
299
|
"""
|
|
259
|
-
Update the instance with any symbols
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
Update the instance with any symbols found in object ``ob``.
|
|
301
|
+
|
|
302
|
+
:param load_all:
|
|
303
|
+
If true private symbols will also be loaded into the options
|
|
304
|
+
object.
|
|
262
305
|
"""
|
|
263
306
|
self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
|
|
264
307
|
|
|
@@ -298,3 +341,70 @@ def parse_key_value_pairs(options, lines: Iterable[str]):
|
|
|
298
341
|
k = k.strip()
|
|
299
342
|
values[k] = options[k] = parse_value(options, v)
|
|
300
343
|
return values
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def list_from_str(s, separator=","):
|
|
347
|
+
"""
|
|
348
|
+
Return the value of ``s`` split into a list of times by ``separator``.
|
|
349
|
+
Spaces around items will be stripped.
|
|
350
|
+
"""
|
|
351
|
+
if isinstance(s, str):
|
|
352
|
+
if not s.strip():
|
|
353
|
+
return []
|
|
354
|
+
return [item.strip() for item in s.split(",")]
|
|
355
|
+
raise TypeError(f"Expected string, got {s!r}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def dict_from_options(
|
|
359
|
+
prefix: str, options: t.Mapping[str, Any], recursive: bool = False
|
|
360
|
+
) -> t.Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Create a dictionary containing all items in ``options`` whose keys start
|
|
363
|
+
with ``prefix``, with that prefix stripped.
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
|
|
367
|
+
>>> from fresco.options import Options, dict_from_options
|
|
368
|
+
>>> options = Options(
|
|
369
|
+
... {"DATABASE_HOST": "127.0.0.1", "DATABASE_USER": "scott"}
|
|
370
|
+
... )
|
|
371
|
+
>>> dict_from_options("DATABASE_", options)
|
|
372
|
+
{'HOST': '127.0.0.1', 'USER': 'scott'}
|
|
373
|
+
|
|
374
|
+
If ``recursive`` is True, a recursive splitting will be
|
|
375
|
+
applied. The last character of ``prefix`` will be used as the separator,
|
|
376
|
+
for example::
|
|
377
|
+
|
|
378
|
+
>>> from fresco.options import dict_from_options
|
|
379
|
+
>>> options = Options(
|
|
380
|
+
... {
|
|
381
|
+
... "DATABASE_DEV_HOST": "127.0.0.1",
|
|
382
|
+
... "DATABASE_DEV_USER": "scott",
|
|
383
|
+
... "DATABASE_PROD_HOST": "192.168.0.78",
|
|
384
|
+
... "DATABASE_PROD_HOST": "tiger",
|
|
385
|
+
... }
|
|
386
|
+
... )
|
|
387
|
+
>>> dict_from_options("DATABASE_", options, recursive=True)
|
|
388
|
+
{'DEV': {'HOST': '127.0.0.1', ...}, 'PROD': {'HOST', ...}}
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
prefixlen = len(prefix)
|
|
393
|
+
d = {k[prefixlen:]: v for k, v in options.items() if k.startswith(prefix)}
|
|
394
|
+
|
|
395
|
+
def recursive_split(d: t.Mapping[str, Any], sep: str) -> t.Dict[str, Any]:
|
|
396
|
+
d_: t.Dict[str, Any] = {}
|
|
397
|
+
for k in d:
|
|
398
|
+
if sep in k:
|
|
399
|
+
ks = k.split(sep)
|
|
400
|
+
subdict = d_
|
|
401
|
+
for subkey in ks[:-1]:
|
|
402
|
+
subdict = subdict.setdefault(subkey, {})
|
|
403
|
+
subdict[ks[-1]] = d[k]
|
|
404
|
+
else:
|
|
405
|
+
d_[k] = d[k]
|
|
406
|
+
return d_
|
|
407
|
+
|
|
408
|
+
if recursive:
|
|
409
|
+
return recursive_split(d, prefix[-1])
|
|
410
|
+
return d
|
|
@@ -137,7 +137,7 @@ class Request(object):
|
|
|
137
137
|
the ``query`` property.
|
|
138
138
|
"""
|
|
139
139
|
if self._form is None:
|
|
140
|
-
if self.environ["REQUEST_METHOD"] in
|
|
140
|
+
if self.environ["REQUEST_METHOD"] in {"PUT", "POST"}:
|
|
141
141
|
items, close = parse_post(
|
|
142
142
|
self.environ,
|
|
143
143
|
self.environ["wsgi.input"],
|