plain 0.24.0__py3-none-any.whl → 0.25.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.
- plain/assets/README.md +5 -5
- plain/cli/README.md +2 -4
- plain/cli/cli.py +6 -6
- plain/csrf/middleware.py +0 -1
- plain/exceptions.py +1 -21
- plain/forms/fields.py +13 -3
- plain/forms/forms.py +0 -1
- plain/http/multipartparser.py +0 -2
- plain/http/request.py +18 -41
- plain/internal/files/base.py +1 -29
- plain/internal/handlers/wsgi.py +18 -1
- plain/paginator.py +0 -4
- plain/preflight/urls.py +0 -7
- plain/urls/resolvers.py +1 -1
- plain/utils/cache.py +0 -202
- plain/utils/encoding.py +0 -105
- plain/utils/functional.py +0 -7
- plain/utils/html.py +1 -276
- plain/utils/http.py +2 -189
- plain/utils/inspect.py +0 -35
- plain/utils/safestring.py +0 -3
- plain/utils/text.py +0 -253
- plain/validators.py +0 -11
- {plain-0.24.0.dist-info → plain-0.25.0.dist-info}/METADATA +1 -1
- {plain-0.24.0.dist-info → plain-0.25.0.dist-info}/RECORD +28 -32
- plain/utils/_os.py +0 -52
- plain/utils/dateformat.py +0 -330
- plain/utils/dates.py +0 -76
- plain/utils/email.py +0 -12
- {plain-0.24.0.dist-info → plain-0.25.0.dist-info}/WHEEL +0 -0
- {plain-0.24.0.dist-info → plain-0.25.0.dist-info}/entry_points.txt +0 -0
- {plain-0.24.0.dist-info → plain-0.25.0.dist-info}/licenses/LICENSE +0 -0
plain/utils/inspect.py
CHANGED
@@ -25,49 +25,14 @@ def get_func_args(func):
|
|
25
25
|
]
|
26
26
|
|
27
27
|
|
28
|
-
def get_func_full_args(func):
|
29
|
-
"""
|
30
|
-
Return a list of (argument name, default value) tuples. If the argument
|
31
|
-
does not have a default value, omit it in the tuple. Arguments such as
|
32
|
-
*args and **kwargs are also included.
|
33
|
-
"""
|
34
|
-
params = _get_callable_parameters(func)
|
35
|
-
args = []
|
36
|
-
for param in params:
|
37
|
-
name = param.name
|
38
|
-
# Ignore 'self'
|
39
|
-
if name == "self":
|
40
|
-
continue
|
41
|
-
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
42
|
-
name = "*" + name
|
43
|
-
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
44
|
-
name = "**" + name
|
45
|
-
if param.default != inspect.Parameter.empty:
|
46
|
-
args.append((name, param.default))
|
47
|
-
else:
|
48
|
-
args.append((name,))
|
49
|
-
return args
|
50
|
-
|
51
|
-
|
52
28
|
def func_accepts_kwargs(func):
|
53
29
|
"""Return True if function 'func' accepts keyword arguments **kwargs."""
|
54
30
|
return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_KEYWORD)
|
55
31
|
|
56
32
|
|
57
|
-
def func_accepts_var_args(func):
|
58
|
-
"""
|
59
|
-
Return True if function 'func' accepts positional arguments *args.
|
60
|
-
"""
|
61
|
-
return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_POSITIONAL)
|
62
|
-
|
63
|
-
|
64
33
|
def method_has_no_args(meth):
|
65
34
|
"""Return True if a method only accepts 'self'."""
|
66
35
|
count = len(
|
67
36
|
[p for p in _get_callable_parameters(meth) if p.kind == p.POSITIONAL_OR_KEYWORD]
|
68
37
|
)
|
69
38
|
return count == 0 if inspect.ismethod(meth) else count == 1
|
70
|
-
|
71
|
-
|
72
|
-
def func_supports_parameter(func, name):
|
73
|
-
return any(param.name == name for param in _get_callable_parameters(func))
|
plain/utils/safestring.py
CHANGED
plain/utils/text.py
CHANGED
@@ -1,65 +1,13 @@
|
|
1
|
-
import gzip
|
2
1
|
import re
|
3
|
-
import secrets
|
4
2
|
import unicodedata
|
5
|
-
from gzip import GzipFile
|
6
|
-
from gzip import compress as gzip_compress
|
7
|
-
from io import BytesIO
|
8
3
|
|
9
|
-
from plain.exceptions import SuspiciousFileOperation
|
10
4
|
from plain.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
|
11
5
|
from plain.utils.regex_helper import _lazy_re_compile
|
12
6
|
|
13
|
-
|
14
|
-
@keep_lazy_text
|
15
|
-
def capfirst(x):
|
16
|
-
"""Capitalize the first letter of a string."""
|
17
|
-
if not x:
|
18
|
-
return x
|
19
|
-
if not isinstance(x, str):
|
20
|
-
x = str(x)
|
21
|
-
return x[0].upper() + x[1:]
|
22
|
-
|
23
|
-
|
24
7
|
# Set up regular expressions
|
25
8
|
re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
|
26
9
|
re_chars = _lazy_re_compile(r"<[^>]+?>|(.)", re.S)
|
27
10
|
re_tag = _lazy_re_compile(r"<(/)?(\S+?)(?:(\s*/)|\s.*?)?>", re.S)
|
28
|
-
re_newlines = _lazy_re_compile(r"\r\n|\r") # Used in normalize_newlines
|
29
|
-
re_camel_case = _lazy_re_compile(r"(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))")
|
30
|
-
|
31
|
-
|
32
|
-
@keep_lazy_text
|
33
|
-
def wrap(text, width):
|
34
|
-
"""
|
35
|
-
A word-wrap function that preserves existing line breaks. Expects that
|
36
|
-
existing line breaks are posix newlines.
|
37
|
-
|
38
|
-
Preserve all white space except added line breaks consume the space on
|
39
|
-
which they break the line.
|
40
|
-
|
41
|
-
Don't wrap long words, thus the output text may have lines longer than
|
42
|
-
``width``.
|
43
|
-
"""
|
44
|
-
|
45
|
-
def _generator():
|
46
|
-
for line in text.splitlines(True): # True keeps trailing linebreaks
|
47
|
-
max_width = min((line.endswith("\n") and width + 1 or width), width)
|
48
|
-
while len(line) > max_width:
|
49
|
-
space = line[: max_width + 1].rfind(" ") + 1
|
50
|
-
if space == 0:
|
51
|
-
space = line.find(" ") + 1
|
52
|
-
if space == 0:
|
53
|
-
yield line
|
54
|
-
line = ""
|
55
|
-
break
|
56
|
-
yield f"{line[: space - 1]}\n"
|
57
|
-
line = line[space:]
|
58
|
-
max_width = min((line.endswith("\n") and width + 1 or width), width)
|
59
|
-
if line:
|
60
|
-
yield line
|
61
|
-
|
62
|
-
return "".join(_generator())
|
63
11
|
|
64
12
|
|
65
13
|
class Truncator(SimpleLazyObject):
|
@@ -229,189 +177,6 @@ class Truncator(SimpleLazyObject):
|
|
229
177
|
return out
|
230
178
|
|
231
179
|
|
232
|
-
@keep_lazy_text
|
233
|
-
def get_valid_filename(name):
|
234
|
-
"""
|
235
|
-
Return the given string converted to a string that can be used for a clean
|
236
|
-
filename. Remove leading and trailing spaces; convert other spaces to
|
237
|
-
underscores; and remove anything that is not an alphanumeric, dash,
|
238
|
-
underscore, or dot.
|
239
|
-
>>> get_valid_filename("john's portrait in 2004.jpg")
|
240
|
-
'johns_portrait_in_2004.jpg'
|
241
|
-
"""
|
242
|
-
s = str(name).strip().replace(" ", "_")
|
243
|
-
s = re.sub(r"(?u)[^-\w.]", "", s)
|
244
|
-
if s in {"", ".", ".."}:
|
245
|
-
raise SuspiciousFileOperation(f"Could not derive file name from '{name}'")
|
246
|
-
return s
|
247
|
-
|
248
|
-
|
249
|
-
@keep_lazy_text
|
250
|
-
def get_text_list(list_, last_word="or"):
|
251
|
-
"""
|
252
|
-
>>> get_text_list(['a', 'b', 'c', 'd'])
|
253
|
-
'a, b, c or d'
|
254
|
-
>>> get_text_list(['a', 'b', 'c'], 'and')
|
255
|
-
'a, b and c'
|
256
|
-
>>> get_text_list(['a', 'b'], 'and')
|
257
|
-
'a and b'
|
258
|
-
>>> get_text_list(['a'])
|
259
|
-
'a'
|
260
|
-
>>> get_text_list([])
|
261
|
-
''
|
262
|
-
"""
|
263
|
-
if not list_:
|
264
|
-
return ""
|
265
|
-
if len(list_) == 1:
|
266
|
-
return str(list_[0])
|
267
|
-
return "{} {} {}".format(
|
268
|
-
# Translators: This string is used as a separator between list elements
|
269
|
-
", ".join(str(i) for i in list_[:-1]),
|
270
|
-
str(last_word),
|
271
|
-
str(list_[-1]),
|
272
|
-
)
|
273
|
-
|
274
|
-
|
275
|
-
@keep_lazy_text
|
276
|
-
def normalize_newlines(text):
|
277
|
-
"""Normalize CRLF and CR newlines to just LF."""
|
278
|
-
return re_newlines.sub("\n", str(text))
|
279
|
-
|
280
|
-
|
281
|
-
@keep_lazy_text
|
282
|
-
def phone2numeric(phone):
|
283
|
-
"""Convert a phone number with letters into its numeric equivalent."""
|
284
|
-
char2number = {
|
285
|
-
"a": "2",
|
286
|
-
"b": "2",
|
287
|
-
"c": "2",
|
288
|
-
"d": "3",
|
289
|
-
"e": "3",
|
290
|
-
"f": "3",
|
291
|
-
"g": "4",
|
292
|
-
"h": "4",
|
293
|
-
"i": "4",
|
294
|
-
"j": "5",
|
295
|
-
"k": "5",
|
296
|
-
"l": "5",
|
297
|
-
"m": "6",
|
298
|
-
"n": "6",
|
299
|
-
"o": "6",
|
300
|
-
"p": "7",
|
301
|
-
"q": "7",
|
302
|
-
"r": "7",
|
303
|
-
"s": "7",
|
304
|
-
"t": "8",
|
305
|
-
"u": "8",
|
306
|
-
"v": "8",
|
307
|
-
"w": "9",
|
308
|
-
"x": "9",
|
309
|
-
"y": "9",
|
310
|
-
"z": "9",
|
311
|
-
}
|
312
|
-
return "".join(char2number.get(c, c) for c in phone.lower())
|
313
|
-
|
314
|
-
|
315
|
-
def _get_random_filename(max_random_bytes):
|
316
|
-
return b"a" * secrets.randbelow(max_random_bytes)
|
317
|
-
|
318
|
-
|
319
|
-
def compress_string(s, *, max_random_bytes=None):
|
320
|
-
compressed_data = gzip_compress(s, compresslevel=6, mtime=0)
|
321
|
-
|
322
|
-
if not max_random_bytes:
|
323
|
-
return compressed_data
|
324
|
-
|
325
|
-
compressed_view = memoryview(compressed_data)
|
326
|
-
header = bytearray(compressed_view[:10])
|
327
|
-
header[3] = gzip.FNAME
|
328
|
-
|
329
|
-
filename = _get_random_filename(max_random_bytes) + b"\x00"
|
330
|
-
|
331
|
-
return bytes(header) + filename + compressed_view[10:]
|
332
|
-
|
333
|
-
|
334
|
-
class StreamingBuffer(BytesIO):
|
335
|
-
def read(self):
|
336
|
-
ret = self.getvalue()
|
337
|
-
self.seek(0)
|
338
|
-
self.truncate()
|
339
|
-
return ret
|
340
|
-
|
341
|
-
|
342
|
-
# Like compress_string, but for iterators of strings.
|
343
|
-
def compress_sequence(sequence, *, max_random_bytes=None):
|
344
|
-
buf = StreamingBuffer()
|
345
|
-
filename = _get_random_filename(max_random_bytes) if max_random_bytes else None
|
346
|
-
with GzipFile(
|
347
|
-
filename=filename, mode="wb", compresslevel=6, fileobj=buf, mtime=0
|
348
|
-
) as zfile:
|
349
|
-
# Output headers...
|
350
|
-
yield buf.read()
|
351
|
-
for item in sequence:
|
352
|
-
zfile.write(item)
|
353
|
-
data = buf.read()
|
354
|
-
if data:
|
355
|
-
yield data
|
356
|
-
yield buf.read()
|
357
|
-
|
358
|
-
|
359
|
-
# Expression to match some_token and some_token="with spaces" (and similarly
|
360
|
-
# for single-quoted strings).
|
361
|
-
smart_split_re = _lazy_re_compile(
|
362
|
-
r"""
|
363
|
-
((?:
|
364
|
-
[^\s'"]*
|
365
|
-
(?:
|
366
|
-
(?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
|
367
|
-
[^\s'"]*
|
368
|
-
)+
|
369
|
-
) | \S+)
|
370
|
-
""",
|
371
|
-
re.VERBOSE,
|
372
|
-
)
|
373
|
-
|
374
|
-
|
375
|
-
def smart_split(text):
|
376
|
-
r"""
|
377
|
-
Generator that splits a string by spaces, leaving quoted phrases together.
|
378
|
-
Supports both single and double quotes, and supports escaping quotes with
|
379
|
-
backslashes. In the output, strings will keep their initial and trailing
|
380
|
-
quote marks and escaped quotes will remain escaped (the results can then
|
381
|
-
be further processed with unescape_string_literal()).
|
382
|
-
|
383
|
-
>>> list(smart_split(r'This is "a person\'s" test.'))
|
384
|
-
['This', 'is', '"a person\\\'s"', 'test.']
|
385
|
-
>>> list(smart_split(r"Another 'person\'s' test."))
|
386
|
-
['Another', "'person\\'s'", 'test.']
|
387
|
-
>>> list(smart_split(r'A "\"funky\" style" test.'))
|
388
|
-
['A', '"\\"funky\\" style"', 'test.']
|
389
|
-
"""
|
390
|
-
for bit in smart_split_re.finditer(str(text)):
|
391
|
-
yield bit[0]
|
392
|
-
|
393
|
-
|
394
|
-
@keep_lazy_text
|
395
|
-
def unescape_string_literal(s):
|
396
|
-
r"""
|
397
|
-
Convert quoted string literals to unquoted strings with escaped quotes and
|
398
|
-
backslashes unquoted::
|
399
|
-
|
400
|
-
>>> unescape_string_literal('"abc"')
|
401
|
-
'abc'
|
402
|
-
>>> unescape_string_literal("'abc'")
|
403
|
-
'abc'
|
404
|
-
>>> unescape_string_literal('"a \"bc\""')
|
405
|
-
'a "bc"'
|
406
|
-
>>> unescape_string_literal("'\'ab\' c'")
|
407
|
-
"'ab' c"
|
408
|
-
"""
|
409
|
-
if not s or s[0] not in "\"'" or s[-1] != s[0]:
|
410
|
-
raise ValueError(f"Not a string literal: {s!r}")
|
411
|
-
quote = s[0]
|
412
|
-
return s[1:-1].replace(rf"\{quote}", quote).replace(r"\\", "\\")
|
413
|
-
|
414
|
-
|
415
180
|
@keep_lazy_text
|
416
181
|
def slugify(value, allow_unicode=False):
|
417
182
|
"""
|
@@ -433,21 +198,6 @@ def slugify(value, allow_unicode=False):
|
|
433
198
|
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
434
199
|
|
435
200
|
|
436
|
-
def camel_case_to_spaces(value):
|
437
|
-
"""
|
438
|
-
Split CamelCase and convert to lowercase. Strip surrounding whitespace.
|
439
|
-
"""
|
440
|
-
return re_camel_case.sub(r" \1", value).strip().lower()
|
441
|
-
|
442
|
-
|
443
|
-
def _format_lazy(format_string, *args, **kwargs):
|
444
|
-
"""
|
445
|
-
Apply str.format() on 'format_string' where format_string, args,
|
446
|
-
and/or kwargs might be lazy.
|
447
|
-
"""
|
448
|
-
return format_string.format(*args, **kwargs)
|
449
|
-
|
450
|
-
|
451
201
|
def pluralize(singular, plural, number):
|
452
202
|
if number == 1:
|
453
203
|
return singular
|
@@ -513,6 +263,3 @@ def pluralize_lazy(singular, plural, number):
|
|
513
263
|
return proxy
|
514
264
|
|
515
265
|
return lazy_number(pluralize, str, singular=singular, plural=plural, number=number)
|
516
|
-
|
517
|
-
|
518
|
-
format_lazy = lazy(_format_lazy, str)
|
plain/validators.py
CHANGED
@@ -158,17 +158,6 @@ class URLValidator(RegexValidator):
|
|
158
158
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
159
159
|
|
160
160
|
|
161
|
-
integer_validator = RegexValidator(
|
162
|
-
_lazy_re_compile(r"^-?\d+\Z"),
|
163
|
-
message="Enter a valid integer.",
|
164
|
-
code="invalid",
|
165
|
-
)
|
166
|
-
|
167
|
-
|
168
|
-
def validate_integer(value):
|
169
|
-
return integer_validator(value)
|
170
|
-
|
171
|
-
|
172
161
|
@deconstructible
|
173
162
|
class EmailValidator:
|
174
163
|
message = "Enter a valid email address."
|
@@ -1,45 +1,45 @@
|
|
1
1
|
plain/README.md,sha256=nW3Ioj3IxPb6aoCGaFMN2n7Cd7LMx0s8Lph6pMkKnh4,8
|
2
2
|
plain/__main__.py,sha256=BiYbF-txGNbeRqp_CHQ9EZ_bCbbKq2iw51Z8RRUgIBY,105
|
3
3
|
plain/debug.py,sha256=abPkJY4aSbBYGEYSZST_ZY3ohXPGDdz9uWQBYRqfd3M,730
|
4
|
-
plain/exceptions.py,sha256=
|
4
|
+
plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
|
5
5
|
plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
|
6
|
-
plain/paginator.py,sha256
|
6
|
+
plain/paginator.py,sha256=-fpLJd6c-V8bLCaNCHfTqPtm-Lm2Y1TuKqFDfy7n3ZE,5857
|
7
7
|
plain/signing.py,sha256=sf7g1Mp-FzdjFAEoLxHyu2YvbUl5w4FOtTVDAfq6TO0,8733
|
8
|
-
plain/validators.py,sha256=
|
8
|
+
plain/validators.py,sha256=byAdFophb3Mfs09IwqzpIgumQHIil76ZDj2suYNaUNQ,19723
|
9
9
|
plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
|
10
|
-
plain/assets/README.md,sha256=
|
10
|
+
plain/assets/README.md,sha256=Ukm_gU7Xj-itAmEjsWUXXDtU5d8BSRpy7ZgGB2LbSo0,2847
|
11
11
|
plain/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
plain/assets/compile.py,sha256=Qg-rMWykij_Jheq4THrPFWlmYv07ihHzWiNsD815HYE,3336
|
13
13
|
plain/assets/finders.py,sha256=rhkHG5QW3H3IlBGHB5WJf9J6VTdDWgUC0qEs6u2Z4RQ,1233
|
14
14
|
plain/assets/fingerprints.py,sha256=1NKAnnXVlncY5iimXztr0NL3RIjBKsNlZRIe6nmItJc,931
|
15
15
|
plain/assets/urls.py,sha256=lW7VzKNzTKY11JqbszhJQ1Yy0HtljZlsHDnnkTPdLOM,992
|
16
16
|
plain/assets/views.py,sha256=z7noLzoelGw_8-MXcvGKjXs9KZ43Tivmy2TIfnZIpgw,9253
|
17
|
-
plain/cli/README.md,sha256=
|
17
|
+
plain/cli/README.md,sha256=CwrqK-NV5ZK1JDYfR4480uHXmJEyxWF6jA_K40_llE8,2366
|
18
18
|
plain/cli/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
|
19
|
-
plain/cli/cli.py,sha256=
|
19
|
+
plain/cli/cli.py,sha256=NbkgCiJk-QaAGUBU7Dne37B_Xv3sZPcGyA5IBG4nFIo,18420
|
20
20
|
plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
|
21
21
|
plain/cli/packages.py,sha256=GLvDgQ1o93tSHae_B2i0YNimpt3LGu4QMQpFYrO48d8,2758
|
22
22
|
plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
|
23
23
|
plain/cli/startup.py,sha256=3LIz9JrIZoF52Sa0j0SCypQwEaBDkhvuGaBdtiQLr5Q,680
|
24
24
|
plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
|
25
|
-
plain/csrf/middleware.py,sha256=
|
25
|
+
plain/csrf/middleware.py,sha256=FYhT7KPJ664Sm0nKjeej1OIXalvVTYiotQX3ytI0dfY,17417
|
26
26
|
plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
|
27
27
|
plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
|
28
28
|
plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
|
29
29
|
plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
|
30
30
|
plain/forms/exceptions.py,sha256=XCLDRl5snIEDu5-8mLB0NnU_tegcBfyIHMiJxqvbxnc,164
|
31
|
-
plain/forms/fields.py,sha256=
|
32
|
-
plain/forms/forms.py,sha256=
|
31
|
+
plain/forms/fields.py,sha256=rsKPsb9cSbvSKhu-eg4yGV1gI-Lv_LaFK_6U22D7oJI,35327
|
32
|
+
plain/forms/forms.py,sha256=fEKBee1b8I_DJ-FufzWJGtSQoUoyieYfqUaGEre9B4Q,10418
|
33
33
|
plain/http/README.md,sha256=HjEtoAhn14OoMdgb-wK-uc8No7C4d4gZUhzseOp7Fg4,236
|
34
34
|
plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
|
35
35
|
plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
|
36
|
-
plain/http/multipartparser.py,sha256=
|
37
|
-
plain/http/request.py,sha256=
|
36
|
+
plain/http/multipartparser.py,sha256=Cyk_UZhxf8JwNza_Yl4_nKCYkmnG7xY9PSVcf9Us57U,27266
|
37
|
+
plain/http/request.py,sha256=kq3AuM0EWyAD_kqMlorTccm5mzIQ6ZefkCa-jXUntnI,25514
|
38
38
|
plain/http/response.py,sha256=RR2sUG-ONWKWcZyIbztjWvtFyh0cR-CoxQvnWOyN0io,23619
|
39
39
|
plain/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
40
|
plain/internal/files/README.md,sha256=kMux-NU5qiH0o1K8IajYQT8VjrYl_jLk9LkGG_kGuSc,45
|
41
41
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
42
|
-
plain/internal/files/base.py,sha256
|
42
|
+
plain/internal/files/base.py,sha256=-JpRMzv2bgVSZ9dcxh13gGRTVeEd_Tjd02iQMOXsRgQ,4126
|
43
43
|
plain/internal/files/locks.py,sha256=z03q7IZD4tPMK3s1HKF3w_uetkFj6w6FTheLUxZsfB0,3616
|
44
44
|
plain/internal/files/move.py,sha256=jfdD29QhamxZjXRgqmZS4dJoJ4sK6M7QK1Km-69jWeo,3238
|
45
45
|
plain/internal/files/temp.py,sha256=UJJnCI8dqPIC8XXHU3-jG2-0svbkrgGlBs4yhciLm4c,2506
|
@@ -49,7 +49,7 @@ plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg
|
|
49
49
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
50
|
plain/internal/handlers/base.py,sha256=oRKni79ATI_u7sywGFExrzKvP5dpJTqIp1m521A90Ew,4169
|
51
51
|
plain/internal/handlers/exception.py,sha256=rv8shMlTJdIhTm99VacILIiu5JRcmtumg8yWuy7GYto,4592
|
52
|
-
plain/internal/handlers/wsgi.py,sha256=
|
52
|
+
plain/internal/handlers/wsgi.py,sha256=aOGCd9hJEMTVMGfgIDlSFvevd8_XCzZa2dtlR4peqZg,8253
|
53
53
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
54
|
plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
|
55
55
|
plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
|
@@ -69,7 +69,7 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
|
|
69
69
|
plain/preflight/messages.py,sha256=HwatjA6MRFfzFAnSOa_uAw1Pvk_CLuNfW3IYi71_1Mk,2322
|
70
70
|
plain/preflight/registry.py,sha256=7s7f_iEwURzv-Ye515P5lJWcHltd5Ca2fsX1Wpbf1wQ,2306
|
71
71
|
plain/preflight/security.py,sha256=sNpv5AHobPcaO48cOUGRNe2EjusTducjY8vyShR8EhI,2645
|
72
|
-
plain/preflight/urls.py,sha256=
|
72
|
+
plain/preflight/urls.py,sha256=OSTLvCpftAD_8VbQ0V3p1CTPlRRwtlnXVBZeWgr7l2k,2881
|
73
73
|
plain/runtime/README.md,sha256=Q8VVO7JRGuYrDxzuYL6ptoilhclbecxKzpRXKgbWGkU,2061
|
74
74
|
plain/runtime/__init__.py,sha256=FyGTIx-633bNPrPv8IBarBKvu_XspbCiqj8W8eOd_mA,1528
|
75
75
|
plain/runtime/global_settings.py,sha256=SfOhwzpZe2zpNqSpdx3hHgCN89xdbW9KJVR4KJfS_Gk,5498
|
@@ -96,35 +96,31 @@ plain/urls/__init__.py,sha256=XF-W2GqLMA4bHbDRKnpZ7tiUtJ-BhWN-yAzw4nNnHdc,590
|
|
96
96
|
plain/urls/converters.py,sha256=s2JZVOdzZC16lgobsI93hygcdH5L0Kj4742WEkXsVcs,1193
|
97
97
|
plain/urls/exceptions.py,sha256=q4iPh3Aa-zHbA-tw8v6WyX1J1n5WdAady2xvxFuyXB0,114
|
98
98
|
plain/urls/patterns.py,sha256=bU_xfhZbKMSgRG9OJ8w_NSuYRm_9zGnqoz_WY44fhUk,9358
|
99
|
-
plain/urls/resolvers.py,sha256=
|
99
|
+
plain/urls/resolvers.py,sha256=3ntfYe2-mQa1tR8jF1-UesnnbZaUp_aVNY3OtiSi5R4,15466
|
100
100
|
plain/urls/routers.py,sha256=J7v-o4BbTk_iPy_kMP_hOMNOPk-D2lockUmSD0Wx1R0,4056
|
101
101
|
plain/urls/utils.py,sha256=WiGq6hHI-5DLFOxCQTAZ2qm0J-UdGosLcjuxlfK6_Tg,2137
|
102
102
|
plain/utils/README.md,sha256=Bf5OG-MkOJDz_U8RGVreDfAI4M4nnPaLtk-LdinxHSc,99
|
103
103
|
plain/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
104
|
-
plain/utils/
|
105
|
-
plain/utils/cache.py,sha256=3GtF8EU07LmD7CSvWSuF7YTThgeNxVkuU68wBK_35Wk,11489
|
104
|
+
plain/utils/cache.py,sha256=0qziMJfzulWnNWlY4MBNfaYHIbKkAXRwy4QZNDr7s3o,4497
|
106
105
|
plain/utils/connection.py,sha256=NN7xRhy6qIWuOOhi1x9YdGcFcYhKepTiMUETeEMS0vY,2501
|
107
106
|
plain/utils/crypto.py,sha256=zFDydnaqNMGYFHUc-CAn8f93685a17BhGusAcITH1lI,2662
|
108
107
|
plain/utils/datastructures.py,sha256=g4UYTbxIb_n8F9JWMP4dHPwUz71591fHreGATPO4qEc,10240
|
109
|
-
plain/utils/dateformat.py,sha256=nsy71l16QuPN0ozGpVlCU5Et101fhk9L38F-wqT5p5I,10203
|
110
108
|
plain/utils/dateparse.py,sha256=u9_tF85YteXSjW9KQzNg_pcCEFDZS3EGorCddcWU0vE,5351
|
111
|
-
plain/utils/dates.py,sha256=hSDKz8eIb3W5QjmGiklFZZELB0inYXsfiRUy49Cx-2Q,1226
|
112
109
|
plain/utils/deconstruct.py,sha256=7NwEFIDCiadAArUBFmiErzDgfIgDWeKqqQFDXwSgQoQ,1830
|
113
110
|
plain/utils/decorators.py,sha256=mLHOo2jLdvYRo2z8lkeVn2vQErlj7xC6XoLwZBYf_z8,358
|
114
111
|
plain/utils/duration.py,sha256=l0Gc41-DeyyAmpdy2XG-YO5UKxMf1NDpWIlQuD5hAn0,1162
|
115
|
-
plain/utils/
|
116
|
-
plain/utils/
|
117
|
-
plain/utils/functional.py,sha256=Xf51mt5bB8zehR8pTRVnRfV21vJ4n1IGpcwj88SSsUA,14791
|
112
|
+
plain/utils/encoding.py,sha256=T0Shb2xRAR3NPwwoqhpUOB55gDprWzqu72aRiiulv9Y,4251
|
113
|
+
plain/utils/functional.py,sha256=9sfuPSX1RalDkLNLjR7k-OjooHXjHSWB9ya2kDLOWtE,14652
|
118
114
|
plain/utils/hashable.py,sha256=uLWobCCh7VcEPJ7xzVGPgigNVuTazYJbyzRzHTCI_wo,739
|
119
|
-
plain/utils/html.py,sha256=
|
120
|
-
plain/utils/http.py,sha256=
|
121
|
-
plain/utils/inspect.py,sha256=
|
115
|
+
plain/utils/html.py,sha256=SR8oNrungB5gxJaHbvAaCw_bAiqLQOk09fj-iIXY0i0,3679
|
116
|
+
plain/utils/http.py,sha256=VOOnwRXnDp5PL_qEmkInLTm10fF58vlhVjeSTdzV2cQ,6031
|
117
|
+
plain/utils/inspect.py,sha256=O3VMH5f4aGOrVpXJBKtQOxx01XrKnjjz6VO_MCV0xkE,1140
|
122
118
|
plain/utils/ipv6.py,sha256=pISQ2AIlG8xXlxpphn388q03fq-fOrlu4GZR0YYjQXw,1267
|
123
119
|
plain/utils/itercompat.py,sha256=lacIDjczhxbwG4ON_KfG1H6VNPOGOpbRhnVhbedo2CY,184
|
124
120
|
plain/utils/module_loading.py,sha256=CWl7Shoax9Zkevf1pM9PpS_0V69J5Cukjyj078UPCAw,2252
|
125
121
|
plain/utils/regex_helper.py,sha256=pAdh_xG52BOyXLsiuIMPFgduUAoWOEje1ZpjhcefxiA,12769
|
126
|
-
plain/utils/safestring.py,sha256=
|
127
|
-
plain/utils/text.py,sha256=
|
122
|
+
plain/utils/safestring.py,sha256=sawOehuWjr4bkF5jXXCcziILQGoqUcA_eEfsURrAyN0,1801
|
123
|
+
plain/utils/text.py,sha256=42hJv06sadbWfsaAHNhqCQaP1W9qZ69trWDTS-Xva7k,9496
|
128
124
|
plain/utils/timesince.py,sha256=a_-ZoPK_s3Pt998CW4rWp0clZ1XyK2x04hCqak2giII,5928
|
129
125
|
plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
|
130
126
|
plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
|
@@ -138,8 +134,8 @@ plain/views/forms.py,sha256=RhlaUcZCkeqokY_fvv-NOS-kgZAG4XhDLOPbf9K_Zlc,2691
|
|
138
134
|
plain/views/objects.py,sha256=g5Lzno0Zsv0K449UpcCtxwCoO7WMRAWqKlxxV2V0_qg,8263
|
139
135
|
plain/views/redirect.py,sha256=9zHZgKvtSkdrMX9KmsRM8hJTPmBktxhc4d8OitbuniI,1724
|
140
136
|
plain/views/templates.py,sha256=cBkFNCSXgVi8cMqQbhsqJ4M_rIQYVl8cUvq9qu4YIes,1951
|
141
|
-
plain-0.
|
142
|
-
plain-0.
|
143
|
-
plain-0.
|
144
|
-
plain-0.
|
145
|
-
plain-0.
|
137
|
+
plain-0.25.0.dist-info/METADATA,sha256=ErgfCIZpGA9-NDGsjKHueIWtZ2LhJquSTQ37ycDo2BQ,319
|
138
|
+
plain-0.25.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
139
|
+
plain-0.25.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
|
140
|
+
plain-0.25.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
141
|
+
plain-0.25.0.dist-info/RECORD,,
|
plain/utils/_os.py
DELETED
@@ -1,52 +0,0 @@
|
|
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
|