plain 0.1.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.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/utils/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Utilities
2
+
3
+ Various utilities for common things like text manipulation, parsing, dates, and more.
File without changes
plain/utils/_os.py ADDED
@@ -0,0 +1,52 @@
1
+ import os
2
+ import tempfile
3
+ from os.path import abspath, dirname, join, normcase, sep
4
+
5
+ from plain.exceptions import SuspiciousFileOperation
6
+
7
+
8
+ def safe_join(base, *paths):
9
+ """
10
+ Join one or more path components to the base path component intelligently.
11
+ Return a normalized, absolute version of the final path.
12
+
13
+ Raise ValueError if the final path isn't located inside of the base path
14
+ component.
15
+ """
16
+ final_path = abspath(join(base, *paths))
17
+ base_path = abspath(base)
18
+ # Ensure final_path starts with base_path (using normcase to ensure we
19
+ # don't false-negative on case insensitive operating systems like Windows),
20
+ # further, one of the following conditions must be true:
21
+ # a) The next character is the path separator (to prevent conditions like
22
+ # safe_join("/dir", "/../d"))
23
+ # b) The final path must be the same as the base path.
24
+ # c) The base path must be the most root path (meaning either "/" or "C:\\")
25
+ if (
26
+ not normcase(final_path).startswith(normcase(base_path + sep))
27
+ and normcase(final_path) != normcase(base_path)
28
+ and dirname(normcase(base_path)) != normcase(base_path)
29
+ ):
30
+ raise SuspiciousFileOperation(
31
+ f"The joined path ({final_path}) is located outside of the base path "
32
+ f"component ({base_path})"
33
+ )
34
+ return final_path
35
+
36
+
37
+ def symlinks_supported():
38
+ """
39
+ Return whether or not creating symlinks are supported in the host platform
40
+ and/or if they are allowed to be created (e.g. on Windows it requires admin
41
+ permissions).
42
+ """
43
+ with tempfile.TemporaryDirectory() as temp_dir:
44
+ original_path = os.path.join(temp_dir, "original")
45
+ symlink_path = os.path.join(temp_dir, "symlink")
46
+ os.makedirs(original_path)
47
+ try:
48
+ os.symlink(original_path, symlink_path)
49
+ supported = True
50
+ except (OSError, NotImplementedError):
51
+ supported = False
52
+ return supported
plain/utils/cache.py ADDED
@@ -0,0 +1,327 @@
1
+ """
2
+ This module contains helper functions for controlling caching. It does so by
3
+ managing the "Vary" header of responses. It includes functions to patch the
4
+ header of response objects directly and decorators that change functions to do
5
+ that header-patching themselves.
6
+
7
+ For information on the Vary header, see RFC 9110 Section 12.5.5.
8
+
9
+ Essentially, the "Vary" HTTP header defines which headers a cache should take
10
+ into account when building its cache key. Requests with the same path but
11
+ different header content for headers named in "Vary" need to get different
12
+ cache keys to prevent delivery of wrong content.
13
+
14
+ An example: i18n middleware would need to distinguish caches by the
15
+ "Accept-language" header.
16
+ """
17
+ import time
18
+ from collections import defaultdict
19
+ from hashlib import md5
20
+
21
+ from plain.http import Response, ResponseNotModified
22
+ from plain.logs import log_response
23
+ from plain.runtime import settings
24
+ from plain.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
25
+ from plain.utils.regex_helper import _lazy_re_compile
26
+
27
+ cc_delim_re = _lazy_re_compile(r"\s*,\s*")
28
+
29
+
30
+ def patch_cache_control(response, **kwargs):
31
+ """
32
+ Patch the Cache-Control header by adding all keyword arguments to it.
33
+ The transformation is as follows:
34
+
35
+ * All keyword parameter names are turned to lowercase, and underscores
36
+ are converted to hyphens.
37
+ * If the value of a parameter is True (exactly True, not just a
38
+ true value), only the parameter name is added to the header.
39
+ * All other parameters are added with their value, after applying
40
+ str() to it.
41
+ """
42
+
43
+ def dictitem(s):
44
+ t = s.split("=", 1)
45
+ if len(t) > 1:
46
+ return (t[0].lower(), t[1])
47
+ else:
48
+ return (t[0].lower(), True)
49
+
50
+ def dictvalue(*t):
51
+ if t[1] is True:
52
+ return t[0]
53
+ else:
54
+ return f"{t[0]}={t[1]}"
55
+
56
+ cc = defaultdict(set)
57
+ if response.get("Cache-Control"):
58
+ for field in cc_delim_re.split(response.headers["Cache-Control"]):
59
+ directive, value = dictitem(field)
60
+ if directive == "no-cache":
61
+ # no-cache supports multiple field names.
62
+ cc[directive].add(value)
63
+ else:
64
+ cc[directive] = value
65
+
66
+ # If there's already a max-age header but we're being asked to set a new
67
+ # max-age, use the minimum of the two ages. In practice this happens when
68
+ # a decorator and a piece of middleware both operate on a given view.
69
+ if "max-age" in cc and "max_age" in kwargs:
70
+ kwargs["max_age"] = min(int(cc["max-age"]), kwargs["max_age"])
71
+
72
+ # Allow overriding private caching and vice versa
73
+ if "private" in cc and "public" in kwargs:
74
+ del cc["private"]
75
+ elif "public" in cc and "private" in kwargs:
76
+ del cc["public"]
77
+
78
+ for k, v in kwargs.items():
79
+ directive = k.replace("_", "-")
80
+ if directive == "no-cache":
81
+ # no-cache supports multiple field names.
82
+ cc[directive].add(v)
83
+ else:
84
+ cc[directive] = v
85
+
86
+ directives = []
87
+ for directive, values in cc.items():
88
+ if isinstance(values, set):
89
+ if True in values:
90
+ # True takes precedence.
91
+ values = {True}
92
+ directives.extend([dictvalue(directive, value) for value in values])
93
+ else:
94
+ directives.append(dictvalue(directive, values))
95
+ cc = ", ".join(directives)
96
+ response.headers["Cache-Control"] = cc
97
+
98
+
99
+ def get_max_age(response):
100
+ """
101
+ Return the max-age from the response Cache-Control header as an integer,
102
+ or None if it wasn't found or wasn't an integer.
103
+ """
104
+ if not response.has_header("Cache-Control"):
105
+ return
106
+ cc = dict(
107
+ _to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
108
+ )
109
+ try:
110
+ return int(cc["max-age"])
111
+ except (ValueError, TypeError, KeyError):
112
+ pass
113
+
114
+
115
+ def set_response_etag(response):
116
+ if not response.streaming and response.content:
117
+ response.headers["ETag"] = quote_etag(
118
+ md5(response.content, usedforsecurity=False).hexdigest(),
119
+ )
120
+ return response
121
+
122
+
123
+ def _precondition_failed(request):
124
+ response = Response(status=412)
125
+ log_response(
126
+ "Precondition Failed: %s",
127
+ request.path,
128
+ response=response,
129
+ request=request,
130
+ )
131
+ return response
132
+
133
+
134
+ def _not_modified(request, response=None):
135
+ new_response = ResponseNotModified()
136
+ if response:
137
+ # Preserve the headers required by RFC 9110 Section 15.4.5, as well as
138
+ # Last-Modified.
139
+ for header in (
140
+ "Cache-Control",
141
+ "Content-Location",
142
+ "Date",
143
+ "ETag",
144
+ "Expires",
145
+ "Last-Modified",
146
+ "Vary",
147
+ ):
148
+ if header in response:
149
+ new_response.headers[header] = response.headers[header]
150
+
151
+ # Preserve cookies as per the cookie specification: "If a proxy server
152
+ # receives a response which contains a Set-cookie header, it should
153
+ # propagate the Set-cookie header to the client, regardless of whether
154
+ # the response was 304 (Not Modified) or 200 (OK).
155
+ # https://curl.haxx.se/rfc/cookie_spec.html
156
+ new_response.cookies = response.cookies
157
+ return new_response
158
+
159
+
160
+ def get_conditional_response(request, etag=None, last_modified=None, response=None):
161
+ # Only return conditional responses on successful requests.
162
+ if response and not (200 <= response.status_code < 300):
163
+ return response
164
+
165
+ # Get HTTP request headers.
166
+ if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", ""))
167
+ if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
168
+ if_unmodified_since = if_unmodified_since and parse_http_date_safe(
169
+ if_unmodified_since
170
+ )
171
+ if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", ""))
172
+ if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
173
+ if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)
174
+
175
+ # Evaluation of request preconditions below follows RFC 9110 Section
176
+ # 13.2.2.
177
+ # Step 1: Test the If-Match precondition.
178
+ if if_match_etags and not _if_match_passes(etag, if_match_etags):
179
+ return _precondition_failed(request)
180
+
181
+ # Step 2: Test the If-Unmodified-Since precondition.
182
+ if (
183
+ not if_match_etags
184
+ and if_unmodified_since
185
+ and not _if_unmodified_since_passes(last_modified, if_unmodified_since)
186
+ ):
187
+ return _precondition_failed(request)
188
+
189
+ # Step 3: Test the If-None-Match precondition.
190
+ if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags):
191
+ if request.method in ("GET", "HEAD"):
192
+ return _not_modified(request, response)
193
+ else:
194
+ return _precondition_failed(request)
195
+
196
+ # Step 4: Test the If-Modified-Since precondition.
197
+ if (
198
+ not if_none_match_etags
199
+ and if_modified_since
200
+ and not _if_modified_since_passes(last_modified, if_modified_since)
201
+ and request.method in ("GET", "HEAD")
202
+ ):
203
+ return _not_modified(request, response)
204
+
205
+ # Step 5: Test the If-Range precondition (not supported).
206
+ # Step 6: Return original response since there isn't a conditional response.
207
+ return response
208
+
209
+
210
+ def _if_match_passes(target_etag, etags):
211
+ """
212
+ Test the If-Match comparison as defined in RFC 9110 Section 13.1.1.
213
+ """
214
+ if not target_etag:
215
+ # If there isn't an ETag, then there can't be a match.
216
+ return False
217
+ elif etags == ["*"]:
218
+ # The existence of an ETag means that there is "a current
219
+ # representation for the target resource", even if the ETag is weak,
220
+ # so there is a match to '*'.
221
+ return True
222
+ elif target_etag.startswith("W/"):
223
+ # A weak ETag can never strongly match another ETag.
224
+ return False
225
+ else:
226
+ # Since the ETag is strong, this will only return True if there's a
227
+ # strong match.
228
+ return target_etag in etags
229
+
230
+
231
+ def _if_unmodified_since_passes(last_modified, if_unmodified_since):
232
+ """
233
+ Test the If-Unmodified-Since comparison as defined in RFC 9110 Section
234
+ 13.1.4.
235
+ """
236
+ return last_modified and last_modified <= if_unmodified_since
237
+
238
+
239
+ def _if_none_match_passes(target_etag, etags):
240
+ """
241
+ Test the If-None-Match comparison as defined in RFC 9110 Section 13.1.2.
242
+ """
243
+ if not target_etag:
244
+ # If there isn't an ETag, then there isn't a match.
245
+ return True
246
+ elif etags == ["*"]:
247
+ # The existence of an ETag means that there is "a current
248
+ # representation for the target resource", so there is a match to '*'.
249
+ return False
250
+ else:
251
+ # The comparison should be weak, so look for a match after stripping
252
+ # off any weak indicators.
253
+ target_etag = target_etag.strip("W/")
254
+ etags = (etag.strip("W/") for etag in etags)
255
+ return target_etag not in etags
256
+
257
+
258
+ def _if_modified_since_passes(last_modified, if_modified_since):
259
+ """
260
+ Test the If-Modified-Since comparison as defined in RFC 9110 Section
261
+ 13.1.3.
262
+ """
263
+ return not last_modified or last_modified > if_modified_since
264
+
265
+
266
+ def patch_response_headers(response, cache_timeout=None):
267
+ """
268
+ Add HTTP caching headers to the given Response: Expires and
269
+ Cache-Control.
270
+
271
+ Each header is only added if it isn't already set.
272
+
273
+ cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
274
+ by default.
275
+ """
276
+ if cache_timeout is None:
277
+ cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
278
+ if cache_timeout < 0:
279
+ cache_timeout = 0 # Can't have max-age negative
280
+ if not response.has_header("Expires"):
281
+ response.headers["Expires"] = http_date(time.time() + cache_timeout)
282
+ patch_cache_control(response, max_age=cache_timeout)
283
+
284
+
285
+ def add_never_cache_headers(response):
286
+ """
287
+ Add headers to a response to indicate that a page should never be cached.
288
+ """
289
+ patch_response_headers(response, cache_timeout=-1)
290
+ patch_cache_control(
291
+ response, no_cache=True, no_store=True, must_revalidate=True, private=True
292
+ )
293
+
294
+
295
+ def patch_vary_headers(response, newheaders):
296
+ """
297
+ Add (or update) the "Vary" header in the given Response object.
298
+ newheaders is a list of header names that should be in "Vary". If headers
299
+ contains an asterisk, then "Vary" header will consist of a single asterisk
300
+ '*'. Otherwise, existing headers in "Vary" aren't removed.
301
+ """
302
+ # Note that we need to keep the original order intact, because cache
303
+ # implementations may rely on the order of the Vary contents in, say,
304
+ # computing an MD5 hash.
305
+ if response.has_header("Vary"):
306
+ vary_headers = cc_delim_re.split(response.headers["Vary"])
307
+ else:
308
+ vary_headers = []
309
+ # Use .lower() here so we treat headers as case-insensitive.
310
+ existing_headers = {header.lower() for header in vary_headers}
311
+ additional_headers = [
312
+ newheader
313
+ for newheader in newheaders
314
+ if newheader.lower() not in existing_headers
315
+ ]
316
+ vary_headers += additional_headers
317
+ if "*" in vary_headers:
318
+ response.headers["Vary"] = "*"
319
+ else:
320
+ response.headers["Vary"] = ", ".join(vary_headers)
321
+
322
+
323
+ def _to_tuple(s):
324
+ t = s.split("=", 1)
325
+ if len(t) == 2:
326
+ return t[0].lower(), t[1]
327
+ return t[0].lower(), True
@@ -0,0 +1,84 @@
1
+ from threading import local
2
+
3
+ from plain.runtime import settings as plain_settings
4
+ from plain.utils.functional import cached_property
5
+
6
+
7
+ class ConnectionProxy:
8
+ """Proxy for accessing a connection object's attributes."""
9
+
10
+ def __init__(self, connections, alias):
11
+ self.__dict__["_connections"] = connections
12
+ self.__dict__["_alias"] = alias
13
+
14
+ def __getattr__(self, item):
15
+ return getattr(self._connections[self._alias], item)
16
+
17
+ def __setattr__(self, name, value):
18
+ return setattr(self._connections[self._alias], name, value)
19
+
20
+ def __delattr__(self, name):
21
+ return delattr(self._connections[self._alias], name)
22
+
23
+ def __contains__(self, key):
24
+ return key in self._connections[self._alias]
25
+
26
+ def __eq__(self, other):
27
+ return self._connections[self._alias] == other
28
+
29
+
30
+ class ConnectionDoesNotExist(Exception):
31
+ pass
32
+
33
+
34
+ class BaseConnectionHandler:
35
+ settings_name = None
36
+ exception_class = ConnectionDoesNotExist
37
+
38
+ def __init__(self, settings=None):
39
+ self._settings = settings
40
+ self._connections = local()
41
+
42
+ @cached_property
43
+ def settings(self):
44
+ self._settings = self.configure_settings(self._settings)
45
+ return self._settings
46
+
47
+ def configure_settings(self, settings):
48
+ if settings is None:
49
+ settings = getattr(plain_settings, self.settings_name)
50
+ return settings
51
+
52
+ def create_connection(self, alias):
53
+ raise NotImplementedError("Subclasses must implement create_connection().")
54
+
55
+ def __getitem__(self, alias):
56
+ try:
57
+ return getattr(self._connections, alias)
58
+ except AttributeError:
59
+ if alias not in self.settings:
60
+ raise self.exception_class(f"The connection '{alias}' doesn't exist.")
61
+ conn = self.create_connection(alias)
62
+ setattr(self._connections, alias, conn)
63
+ return conn
64
+
65
+ def __setitem__(self, key, value):
66
+ setattr(self._connections, key, value)
67
+
68
+ def __delitem__(self, key):
69
+ delattr(self._connections, key)
70
+
71
+ def __iter__(self):
72
+ return iter(self.settings)
73
+
74
+ def all(self, initialized_only=False):
75
+ return [
76
+ self[alias]
77
+ for alias in self
78
+ # If initialized_only is True, return only initialized connections.
79
+ if not initialized_only or hasattr(self._connections, alias)
80
+ ]
81
+
82
+ def close_all(self):
83
+ for conn in self.all(initialized_only=True):
84
+ conn.close()
plain/utils/crypto.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ Plain's standard crypto functions and utilities.
3
+ """
4
+ import hashlib
5
+ import hmac
6
+ import secrets
7
+
8
+ from plain.runtime import settings
9
+ from plain.utils.encoding import force_bytes
10
+
11
+
12
+ class InvalidAlgorithm(ValueError):
13
+ """Algorithm is not supported by hashlib."""
14
+
15
+ pass
16
+
17
+
18
+ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
19
+ """
20
+ Return the HMAC of 'value', using a key generated from key_salt and a
21
+ secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
22
+ but any algorithm name supported by hashlib can be passed.
23
+
24
+ A different key_salt should be passed in for every application of HMAC.
25
+ """
26
+ if secret is None:
27
+ secret = settings.SECRET_KEY
28
+
29
+ key_salt = force_bytes(key_salt)
30
+ secret = force_bytes(secret)
31
+ try:
32
+ hasher = getattr(hashlib, algorithm)
33
+ except AttributeError as e:
34
+ raise InvalidAlgorithm(
35
+ "%r is not an algorithm accepted by the hashlib module." % algorithm
36
+ ) from e
37
+ # We need to generate a derived key from our base key. We can do this by
38
+ # passing the key_salt and our base key through a pseudo-random function.
39
+ key = hasher(key_salt + secret).digest()
40
+ # If len(key_salt + secret) > block size of the hash algorithm, the above
41
+ # line is redundant and could be replaced by key = key_salt + secret, since
42
+ # the hmac module does the same thing for keys longer than the block size.
43
+ # However, we need to ensure that we *always* do this.
44
+ return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
45
+
46
+
47
+ RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
48
+
49
+
50
+ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
51
+ """
52
+ Return a securely generated random string.
53
+
54
+ The bit length of the returned value can be calculated with the formula:
55
+ log_2(len(allowed_chars)^length)
56
+
57
+ For example, with default `allowed_chars` (26+26+10), this gives:
58
+ * length: 12, bit length =~ 71 bits
59
+ * length: 22, bit length =~ 131 bits
60
+ """
61
+ return "".join(secrets.choice(allowed_chars) for i in range(length))
62
+
63
+
64
+ def constant_time_compare(val1, val2):
65
+ """Return True if the two strings are equal, False otherwise."""
66
+ return secrets.compare_digest(force_bytes(val1), force_bytes(val2))
67
+
68
+
69
+ def pbkdf2(password, salt, iterations, dklen=0, digest=None):
70
+ """Return the hash of password using pbkdf2."""
71
+ if digest is None:
72
+ digest = hashlib.sha256
73
+ dklen = dklen or None
74
+ password = force_bytes(password)
75
+ salt = force_bytes(salt)
76
+ return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)