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.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sets up the terminal color scheme.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from plain.utils import termcolors
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import colorama
|
|
13
|
+
|
|
14
|
+
colorama.init()
|
|
15
|
+
except (ImportError, OSError):
|
|
16
|
+
HAS_COLORAMA = False
|
|
17
|
+
else:
|
|
18
|
+
HAS_COLORAMA = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def supports_color():
|
|
22
|
+
"""
|
|
23
|
+
Return True if the running system's terminal supports color,
|
|
24
|
+
and False otherwise.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def vt_codes_enabled_in_windows_registry():
|
|
28
|
+
"""
|
|
29
|
+
Check the Windows Registry to see if VT code handling has been enabled
|
|
30
|
+
by default, see https://superuser.com/a/1300251/447564.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
# winreg is only available on Windows.
|
|
34
|
+
import winreg
|
|
35
|
+
except ImportError:
|
|
36
|
+
return False
|
|
37
|
+
else:
|
|
38
|
+
try:
|
|
39
|
+
reg_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Console")
|
|
40
|
+
reg_key_value, _ = winreg.QueryValueEx(reg_key, "VirtualTerminalLevel")
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
return False
|
|
43
|
+
else:
|
|
44
|
+
return reg_key_value == 1
|
|
45
|
+
|
|
46
|
+
# isatty is not always implemented, #6223.
|
|
47
|
+
is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
48
|
+
|
|
49
|
+
return is_a_tty and (
|
|
50
|
+
sys.platform != "win32"
|
|
51
|
+
or HAS_COLORAMA
|
|
52
|
+
or "ANSICON" in os.environ
|
|
53
|
+
or
|
|
54
|
+
# Windows Terminal supports VT codes.
|
|
55
|
+
"WT_SESSION" in os.environ
|
|
56
|
+
or
|
|
57
|
+
# Microsoft Visual Studio Code's built-in terminal supports colors.
|
|
58
|
+
os.environ.get("TERM_PROGRAM") == "vscode"
|
|
59
|
+
or vt_codes_enabled_in_windows_registry()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Style:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def make_style(config_string=""):
|
|
68
|
+
"""
|
|
69
|
+
Create a Style object from the given config_string.
|
|
70
|
+
|
|
71
|
+
If config_string is empty plain.utils.termcolors.DEFAULT_PALETTE is used.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
style = Style()
|
|
75
|
+
|
|
76
|
+
color_settings = termcolors.parse_color_setting(config_string)
|
|
77
|
+
|
|
78
|
+
# The nocolor palette has all available roles.
|
|
79
|
+
# Use that palette as the basis for populating
|
|
80
|
+
# the palette as defined in the environment.
|
|
81
|
+
for role in termcolors.PALETTES[termcolors.NOCOLOR_PALETTE]:
|
|
82
|
+
if color_settings:
|
|
83
|
+
format = color_settings.get(role, {})
|
|
84
|
+
style_func = termcolors.make_style(**format)
|
|
85
|
+
else:
|
|
86
|
+
|
|
87
|
+
def style_func(x):
|
|
88
|
+
return x
|
|
89
|
+
|
|
90
|
+
setattr(style, role, style_func)
|
|
91
|
+
|
|
92
|
+
# For backwards compatibility,
|
|
93
|
+
# set style for ERROR_OUTPUT == ERROR
|
|
94
|
+
style.ERROR_OUTPUT = style.ERROR
|
|
95
|
+
|
|
96
|
+
return style
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@functools.cache
|
|
100
|
+
def no_style():
|
|
101
|
+
"""
|
|
102
|
+
Return a Style object with no color scheme.
|
|
103
|
+
"""
|
|
104
|
+
return make_style("nocolor")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def color_style(force_color=False):
|
|
108
|
+
"""
|
|
109
|
+
Return a Style object from the Plain color scheme.
|
|
110
|
+
"""
|
|
111
|
+
if not force_color and not supports_color():
|
|
112
|
+
return no_style()
|
|
113
|
+
return make_style(os.environ.get("DJANGO_COLORS", ""))
|
|
File without changes
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from plain.assets.finders import get_finders
|
|
4
|
+
from plain.assets.storage import FileSystemStorage, assets_storage
|
|
5
|
+
from plain.internal.legacy.management.base import BaseCommand, CommandError
|
|
6
|
+
from plain.internal.legacy.management.color import no_style
|
|
7
|
+
from plain.utils.functional import cached_property
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(BaseCommand):
|
|
11
|
+
"""
|
|
12
|
+
Copies static files from different locations to the
|
|
13
|
+
settings.ASSETS_ROOT.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
help = "Collect static files in a single location."
|
|
17
|
+
# requires_system_checks = [Tags.assets]
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.copied_files = []
|
|
22
|
+
self.unmodified_files = []
|
|
23
|
+
self.post_processed_files = []
|
|
24
|
+
self.storage = assets_storage
|
|
25
|
+
self.style = no_style()
|
|
26
|
+
|
|
27
|
+
@cached_property
|
|
28
|
+
def local(self):
|
|
29
|
+
try:
|
|
30
|
+
self.storage.path("")
|
|
31
|
+
except NotImplementedError:
|
|
32
|
+
return False
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def add_arguments(self, parser):
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--noinput",
|
|
38
|
+
"--no-input",
|
|
39
|
+
action="store_false",
|
|
40
|
+
dest="interactive",
|
|
41
|
+
help="Do NOT prompt the user for input of any kind.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--no-post-process",
|
|
45
|
+
action="store_false",
|
|
46
|
+
dest="post_process",
|
|
47
|
+
help="Do NOT post process collected files.",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-i",
|
|
51
|
+
"--ignore",
|
|
52
|
+
action="append",
|
|
53
|
+
default=[],
|
|
54
|
+
dest="ignore_patterns",
|
|
55
|
+
metavar="PATTERN",
|
|
56
|
+
help="Ignore files or directories matching this glob-style "
|
|
57
|
+
"pattern. Use multiple times to ignore more.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"-n",
|
|
61
|
+
"--dry-run",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Do everything except modify the filesystem.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"-c",
|
|
67
|
+
"--clear",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Clear the existing files using the storage "
|
|
70
|
+
"before trying to copy or link the original file.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--no-default-ignore",
|
|
74
|
+
action="store_false",
|
|
75
|
+
dest="use_default_ignore_patterns",
|
|
76
|
+
help=(
|
|
77
|
+
"Don't ignore the common private glob-style patterns (defaults to "
|
|
78
|
+
"'CVS', '.*' and '*~')."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def set_options(self, **options):
|
|
83
|
+
"""
|
|
84
|
+
Set instance variables based on an options dict
|
|
85
|
+
"""
|
|
86
|
+
self.interactive = options["interactive"]
|
|
87
|
+
self.verbosity = options["verbosity"]
|
|
88
|
+
self.clear = options["clear"]
|
|
89
|
+
self.dry_run = options["dry_run"]
|
|
90
|
+
ignore_patterns = options["ignore_patterns"]
|
|
91
|
+
if options["use_default_ignore_patterns"]:
|
|
92
|
+
ignore_patterns += ["CVS", ".*", "*~"]
|
|
93
|
+
self.ignore_patterns = list({os.path.normpath(p) for p in ignore_patterns})
|
|
94
|
+
self.post_process = options["post_process"]
|
|
95
|
+
|
|
96
|
+
def collect(self):
|
|
97
|
+
"""
|
|
98
|
+
Perform the bulk of the work of collectstatic.
|
|
99
|
+
|
|
100
|
+
Split off from handle() to facilitate testing.
|
|
101
|
+
"""
|
|
102
|
+
if self.clear:
|
|
103
|
+
self.clear_dir("")
|
|
104
|
+
|
|
105
|
+
found_files = {}
|
|
106
|
+
for finder in get_finders():
|
|
107
|
+
for path, storage in finder.list(self.ignore_patterns):
|
|
108
|
+
# Prefix the relative path if the source storage contains it
|
|
109
|
+
if getattr(storage, "prefix", None):
|
|
110
|
+
prefixed_path = os.path.join(storage.prefix, path)
|
|
111
|
+
else:
|
|
112
|
+
prefixed_path = path
|
|
113
|
+
|
|
114
|
+
if prefixed_path not in found_files:
|
|
115
|
+
found_files[prefixed_path] = (storage, path)
|
|
116
|
+
self.copy_file(path, prefixed_path, storage)
|
|
117
|
+
else:
|
|
118
|
+
self.log(
|
|
119
|
+
"Found another file with the destination path '%s'. It "
|
|
120
|
+
"will be ignored since only the first encountered file "
|
|
121
|
+
"is collected. If this is not what you want, make sure "
|
|
122
|
+
"every static file has a unique path." % prefixed_path,
|
|
123
|
+
level=1,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Storage backends may define a post_process() method.
|
|
127
|
+
if self.post_process and hasattr(self.storage, "post_process"):
|
|
128
|
+
processor = self.storage.post_process(found_files, dry_run=self.dry_run)
|
|
129
|
+
for original_path, processed_path, processed in processor:
|
|
130
|
+
if isinstance(processed, Exception):
|
|
131
|
+
self.stderr.write("Post-processing '%s' failed!" % original_path)
|
|
132
|
+
# Add a blank line before the traceback, otherwise it's
|
|
133
|
+
# too easy to miss the relevant part of the error message.
|
|
134
|
+
self.stderr.write()
|
|
135
|
+
raise processed
|
|
136
|
+
if processed:
|
|
137
|
+
self.log(
|
|
138
|
+
f"Post-processed '{original_path}' as '{processed_path}'",
|
|
139
|
+
level=2,
|
|
140
|
+
)
|
|
141
|
+
self.post_processed_files.append(original_path)
|
|
142
|
+
else:
|
|
143
|
+
self.log("Skipped post-processing '%s'" % original_path)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"modified": self.copied_files,
|
|
147
|
+
"unmodified": self.unmodified_files,
|
|
148
|
+
"post_processed": self.post_processed_files,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def handle(self, **options):
|
|
152
|
+
self.set_options(**options)
|
|
153
|
+
message = ["\n"]
|
|
154
|
+
if self.dry_run:
|
|
155
|
+
message.append(
|
|
156
|
+
"You have activated the --dry-run option so no files will be "
|
|
157
|
+
"modified.\n\n"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
message.append(
|
|
161
|
+
"You have requested to collect static files at the destination\n"
|
|
162
|
+
"location as specified in your settings"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if self.is_local_storage() and self.storage.location:
|
|
166
|
+
destination_path = self.storage.location
|
|
167
|
+
message.append(":\n\n %s\n\n" % destination_path)
|
|
168
|
+
should_warn_user = self.storage.exists(destination_path) and any(
|
|
169
|
+
self.storage.listdir(destination_path)
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
destination_path = None
|
|
173
|
+
message.append(".\n\n")
|
|
174
|
+
# Destination files existence not checked; play it safe and warn.
|
|
175
|
+
should_warn_user = True
|
|
176
|
+
|
|
177
|
+
if self.interactive and should_warn_user:
|
|
178
|
+
if self.clear:
|
|
179
|
+
message.append("This will DELETE ALL FILES in this location!\n")
|
|
180
|
+
else:
|
|
181
|
+
message.append("This will overwrite existing files!\n")
|
|
182
|
+
|
|
183
|
+
message.append(
|
|
184
|
+
"Are you sure you want to do this?\n\n"
|
|
185
|
+
"Type 'yes' to continue, or 'no' to cancel: "
|
|
186
|
+
)
|
|
187
|
+
if input("".join(message)) != "yes":
|
|
188
|
+
raise CommandError("Collecting static files cancelled.")
|
|
189
|
+
|
|
190
|
+
collected = self.collect()
|
|
191
|
+
|
|
192
|
+
if self.verbosity >= 1:
|
|
193
|
+
modified_count = len(collected["modified"])
|
|
194
|
+
unmodified_count = len(collected["unmodified"])
|
|
195
|
+
post_processed_count = len(collected["post_processed"])
|
|
196
|
+
return (
|
|
197
|
+
"\n{modified_count} {identifier} {action}"
|
|
198
|
+
"{destination}{unmodified}{post_processed}."
|
|
199
|
+
).format(
|
|
200
|
+
modified_count=modified_count,
|
|
201
|
+
identifier="static file" + ("" if modified_count == 1 else "s"),
|
|
202
|
+
action="copied",
|
|
203
|
+
destination=" to '%s'" % destination_path if destination_path else "",
|
|
204
|
+
unmodified=", %s unmodified" % unmodified_count
|
|
205
|
+
if collected["unmodified"]
|
|
206
|
+
else "",
|
|
207
|
+
post_processed=collected["post_processed"]
|
|
208
|
+
and ", %s post-processed" % post_processed_count
|
|
209
|
+
or "",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def log(self, msg, level=2):
|
|
213
|
+
"""
|
|
214
|
+
Small log helper
|
|
215
|
+
"""
|
|
216
|
+
if self.verbosity >= level:
|
|
217
|
+
self.stdout.write(msg)
|
|
218
|
+
|
|
219
|
+
def is_local_storage(self):
|
|
220
|
+
return isinstance(self.storage, FileSystemStorage)
|
|
221
|
+
|
|
222
|
+
def clear_dir(self, path):
|
|
223
|
+
"""
|
|
224
|
+
Delete the given relative path using the destination storage backend.
|
|
225
|
+
"""
|
|
226
|
+
if not self.storage.exists(path):
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
dirs, files = self.storage.listdir(path)
|
|
230
|
+
for f in files:
|
|
231
|
+
fpath = os.path.join(path, f)
|
|
232
|
+
if self.dry_run:
|
|
233
|
+
self.log("Pretending to delete '%s'" % fpath, level=1)
|
|
234
|
+
else:
|
|
235
|
+
self.log("Deleting '%s'" % fpath, level=1)
|
|
236
|
+
self.storage.delete(fpath)
|
|
237
|
+
for d in dirs:
|
|
238
|
+
self.clear_dir(os.path.join(path, d))
|
|
239
|
+
|
|
240
|
+
def delete_file(self, path, prefixed_path, source_storage):
|
|
241
|
+
"""
|
|
242
|
+
Check if the target file should be deleted if it already exists.
|
|
243
|
+
"""
|
|
244
|
+
if self.storage.exists(prefixed_path):
|
|
245
|
+
try:
|
|
246
|
+
# When was the target file modified last time?
|
|
247
|
+
target_last_modified = self.storage.get_modified_time(prefixed_path)
|
|
248
|
+
except (OSError, NotImplementedError, AttributeError):
|
|
249
|
+
# The storage doesn't support get_modified_time() or failed
|
|
250
|
+
pass
|
|
251
|
+
else:
|
|
252
|
+
try:
|
|
253
|
+
# When was the source file modified last time?
|
|
254
|
+
source_last_modified = source_storage.get_modified_time(path)
|
|
255
|
+
except (OSError, NotImplementedError, AttributeError):
|
|
256
|
+
pass
|
|
257
|
+
else:
|
|
258
|
+
# In remote storages, skipping is only based on the
|
|
259
|
+
# modified times since symlinks aren't relevant.
|
|
260
|
+
can_skip_unmodified_files = not self.local
|
|
261
|
+
# Avoid sub-second precision (see #14665, #19540)
|
|
262
|
+
file_is_unmodified = target_last_modified.replace(
|
|
263
|
+
microsecond=0
|
|
264
|
+
) >= source_last_modified.replace(microsecond=0)
|
|
265
|
+
if file_is_unmodified and can_skip_unmodified_files:
|
|
266
|
+
if prefixed_path not in self.unmodified_files:
|
|
267
|
+
self.unmodified_files.append(prefixed_path)
|
|
268
|
+
self.log("Skipping '%s' (not modified)" % path)
|
|
269
|
+
return False
|
|
270
|
+
# Then delete the existing file if really needed
|
|
271
|
+
if self.dry_run:
|
|
272
|
+
self.log("Pretending to delete '%s'" % path)
|
|
273
|
+
else:
|
|
274
|
+
self.log("Deleting '%s'" % path)
|
|
275
|
+
self.storage.delete(prefixed_path)
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
def copy_file(self, path, prefixed_path, source_storage):
|
|
279
|
+
"""
|
|
280
|
+
Attempt to copy ``path`` with storage
|
|
281
|
+
"""
|
|
282
|
+
# Skip this file if it was already copied earlier
|
|
283
|
+
if prefixed_path in self.copied_files:
|
|
284
|
+
return self.log("Skipping '%s' (already copied earlier)" % path)
|
|
285
|
+
# Delete the target file if needed or break
|
|
286
|
+
if not self.delete_file(path, prefixed_path, source_storage):
|
|
287
|
+
return
|
|
288
|
+
# The full path of the source file
|
|
289
|
+
source_path = source_storage.path(path)
|
|
290
|
+
# Finally start copying
|
|
291
|
+
if self.dry_run:
|
|
292
|
+
self.log("Pretending to copy '%s'" % source_path, level=1)
|
|
293
|
+
else:
|
|
294
|
+
self.log("Copying '%s'" % source_path, level=2)
|
|
295
|
+
with source_storage.open(path) as source_file:
|
|
296
|
+
self.storage.save(prefixed_path, source_file)
|
|
297
|
+
self.copied_files.append(prefixed_path)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from plain.packages import packages
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sql_flush(style, connection, reset_sequences=True, allow_cascade=False):
|
|
7
|
+
"""
|
|
8
|
+
Return a list of the SQL statements used to flush the database.
|
|
9
|
+
"""
|
|
10
|
+
tables = connection.introspection.plain_table_names(
|
|
11
|
+
only_existing=True, include_views=False
|
|
12
|
+
)
|
|
13
|
+
return connection.ops.sql_flush(
|
|
14
|
+
style,
|
|
15
|
+
tables,
|
|
16
|
+
reset_sequences=reset_sequences,
|
|
17
|
+
allow_cascade=allow_cascade,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs):
|
|
22
|
+
# Emit the pre_migrate signal for every application.
|
|
23
|
+
for package_config in packages.get_package_configs():
|
|
24
|
+
if package_config.models_module is None:
|
|
25
|
+
continue
|
|
26
|
+
if verbosity >= 2:
|
|
27
|
+
stdout = kwargs.get("stdout", sys.stdout)
|
|
28
|
+
stdout.write(
|
|
29
|
+
"Running pre-migrate handlers for application %s" % package_config.label
|
|
30
|
+
)
|
|
31
|
+
try:
|
|
32
|
+
from plain import models
|
|
33
|
+
except ImportError:
|
|
34
|
+
continue
|
|
35
|
+
models.signals.pre_migrate.send(
|
|
36
|
+
sender=package_config,
|
|
37
|
+
package_config=package_config,
|
|
38
|
+
verbosity=verbosity,
|
|
39
|
+
interactive=interactive,
|
|
40
|
+
using=db,
|
|
41
|
+
**kwargs,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def emit_post_migrate_signal(verbosity, interactive, db, **kwargs):
|
|
46
|
+
# Emit the post_migrate signal for every application.
|
|
47
|
+
for package_config in packages.get_package_configs():
|
|
48
|
+
if package_config.models_module is None:
|
|
49
|
+
continue
|
|
50
|
+
if verbosity >= 2:
|
|
51
|
+
stdout = kwargs.get("stdout", sys.stdout)
|
|
52
|
+
stdout.write(
|
|
53
|
+
"Running post-migrate handlers for application %s"
|
|
54
|
+
% package_config.label
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
from plain import models
|
|
58
|
+
except ImportError:
|
|
59
|
+
continue
|
|
60
|
+
models.signals.post_migrate.send(
|
|
61
|
+
sender=package_config,
|
|
62
|
+
package_config=package_config,
|
|
63
|
+
verbosity=verbosity,
|
|
64
|
+
interactive=interactive,
|
|
65
|
+
using=db,
|
|
66
|
+
**kwargs,
|
|
67
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from subprocess import run
|
|
7
|
+
|
|
8
|
+
from plain.packages import packages as installed_packages
|
|
9
|
+
from plain.utils.crypto import get_random_string
|
|
10
|
+
from plain.utils.encoding import DEFAULT_LOCALE_ENCODING
|
|
11
|
+
|
|
12
|
+
from .base import CommandError, CommandParser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def popen_wrapper(args, stdout_encoding="utf-8"):
|
|
16
|
+
"""
|
|
17
|
+
Friendly wrapper around Popen.
|
|
18
|
+
|
|
19
|
+
Return stdout output, stderr output, and OS status code.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
p = run(args, capture_output=True, close_fds=os.name != "nt")
|
|
23
|
+
except OSError as err:
|
|
24
|
+
raise CommandError("Error executing %s" % args[0]) from err
|
|
25
|
+
return (
|
|
26
|
+
p.stdout.decode(stdout_encoding),
|
|
27
|
+
p.stderr.decode(DEFAULT_LOCALE_ENCODING, errors="replace"),
|
|
28
|
+
p.returncode,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def handle_extensions(extensions):
|
|
33
|
+
"""
|
|
34
|
+
Organize multiple extensions that are separated with commas or passed by
|
|
35
|
+
using --extension/-e multiple times.
|
|
36
|
+
|
|
37
|
+
For example: running 'django-admin makemessages -e js,txt -e xhtml -a'
|
|
38
|
+
would result in an extension list: ['.js', '.txt', '.xhtml']
|
|
39
|
+
|
|
40
|
+
>>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
|
|
41
|
+
{'.html', '.js', '.py'}
|
|
42
|
+
>>> handle_extensions(['.html, txt,.tpl'])
|
|
43
|
+
{'.html', '.tpl', '.txt'}
|
|
44
|
+
"""
|
|
45
|
+
ext_list = []
|
|
46
|
+
for ext in extensions:
|
|
47
|
+
ext_list.extend(ext.replace(" ", "").split(","))
|
|
48
|
+
for i, ext in enumerate(ext_list):
|
|
49
|
+
if not ext.startswith("."):
|
|
50
|
+
ext_list[i] = ".%s" % ext_list[i]
|
|
51
|
+
return set(ext_list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_command(cmd, path=None, pathext=None):
|
|
55
|
+
if path is None:
|
|
56
|
+
path = os.environ.get("PATH", "").split(os.pathsep)
|
|
57
|
+
if isinstance(path, str):
|
|
58
|
+
path = [path]
|
|
59
|
+
# check if there are funny path extensions for executables, e.g. Windows
|
|
60
|
+
if pathext is None:
|
|
61
|
+
pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(os.pathsep)
|
|
62
|
+
# don't use extensions if the command ends with one of them
|
|
63
|
+
for ext in pathext:
|
|
64
|
+
if cmd.endswith(ext):
|
|
65
|
+
pathext = [""]
|
|
66
|
+
break
|
|
67
|
+
# check if we find the command on PATH
|
|
68
|
+
for p in path:
|
|
69
|
+
f = os.path.join(p, cmd)
|
|
70
|
+
if os.path.isfile(f):
|
|
71
|
+
return f
|
|
72
|
+
for ext in pathext:
|
|
73
|
+
fext = f + ext
|
|
74
|
+
if os.path.isfile(fext):
|
|
75
|
+
return fext
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_random_secret_key():
|
|
80
|
+
"""
|
|
81
|
+
Return a 50 character random string usable as a SECRET_KEY setting value.
|
|
82
|
+
"""
|
|
83
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
|
84
|
+
return get_random_string(50, chars)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_packages_and_model_labels(labels):
|
|
88
|
+
"""
|
|
89
|
+
Parse a list of "package_label.ModelName" or "package_label" strings into actual
|
|
90
|
+
objects and return a two-element tuple:
|
|
91
|
+
(set of model classes, set of package_configs).
|
|
92
|
+
Raise a CommandError if some specified models or packages don't exist.
|
|
93
|
+
"""
|
|
94
|
+
packages = set()
|
|
95
|
+
models = set()
|
|
96
|
+
|
|
97
|
+
for label in labels:
|
|
98
|
+
if "." in label:
|
|
99
|
+
try:
|
|
100
|
+
model = installed_packages.get_model(label)
|
|
101
|
+
except LookupError:
|
|
102
|
+
raise CommandError("Unknown model: %s" % label)
|
|
103
|
+
models.add(model)
|
|
104
|
+
else:
|
|
105
|
+
try:
|
|
106
|
+
package_config = installed_packages.get_package_config(label)
|
|
107
|
+
except LookupError as e:
|
|
108
|
+
raise CommandError(str(e))
|
|
109
|
+
packages.add(package_config)
|
|
110
|
+
|
|
111
|
+
return models, packages
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_command_line_option(argv, option):
|
|
115
|
+
"""
|
|
116
|
+
Return the value of a command line option (which should include leading
|
|
117
|
+
dashes, e.g. '--testrunner') from an argument list. Return None if the
|
|
118
|
+
option wasn't passed or if the argument list couldn't be parsed.
|
|
119
|
+
"""
|
|
120
|
+
parser = CommandParser(add_help=False, allow_abbrev=False)
|
|
121
|
+
parser.add_argument(option, dest="value")
|
|
122
|
+
try:
|
|
123
|
+
options, _ = parser.parse_known_args(argv[2:])
|
|
124
|
+
except CommandError:
|
|
125
|
+
return None
|
|
126
|
+
else:
|
|
127
|
+
return options.value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def normalize_path_patterns(patterns):
|
|
131
|
+
"""Normalize an iterable of glob style patterns based on OS."""
|
|
132
|
+
patterns = [os.path.normcase(p) for p in patterns]
|
|
133
|
+
dir_suffixes = {"%s*" % path_sep for path_sep in {"/", os.sep}}
|
|
134
|
+
norm_patterns = []
|
|
135
|
+
for pattern in patterns:
|
|
136
|
+
for dir_suffix in dir_suffixes:
|
|
137
|
+
if pattern.endswith(dir_suffix):
|
|
138
|
+
norm_patterns.append(pattern.removesuffix(dir_suffix))
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
norm_patterns.append(pattern)
|
|
142
|
+
return norm_patterns
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_ignored_path(path, ignore_patterns):
|
|
146
|
+
"""
|
|
147
|
+
Check if the given path should be ignored or not based on matching
|
|
148
|
+
one of the glob style `ignore_patterns`.
|
|
149
|
+
"""
|
|
150
|
+
path = Path(path)
|
|
151
|
+
|
|
152
|
+
def ignore(pattern):
|
|
153
|
+
return fnmatch.fnmatchcase(path.name, pattern) or fnmatch.fnmatchcase(
|
|
154
|
+
str(path), pattern
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return any(ignore(pattern) for pattern in normalize_path_patterns(ignore_patterns))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def find_formatters():
|
|
161
|
+
return {"black_path": shutil.which("black")}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_formatters(written_files, black_path=(sentinel := object())):
|
|
165
|
+
"""
|
|
166
|
+
Run the black formatter on the specified files.
|
|
167
|
+
"""
|
|
168
|
+
# Use a sentinel rather than None, as which() returns None when not found.
|
|
169
|
+
if black_path is sentinel:
|
|
170
|
+
black_path = shutil.which("black")
|
|
171
|
+
if black_path:
|
|
172
|
+
subprocess.run(
|
|
173
|
+
[black_path, "--fast", "--", *written_files],
|
|
174
|
+
capture_output=True,
|
|
175
|
+
)
|
plain/json.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import decimal
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from plain.utils.duration import duration_iso_string
|
|
7
|
+
from plain.utils.functional import Promise
|
|
8
|
+
from plain.utils.timezone import is_aware
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlainJSONEncoder(json.JSONEncoder):
|
|
12
|
+
"""
|
|
13
|
+
JSONEncoder subclass that knows how to encode date/time, decimal types, and
|
|
14
|
+
UUIDs.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def default(self, o):
|
|
18
|
+
# See "Date Time String Format" in the ECMA-262 specification.
|
|
19
|
+
if isinstance(o, datetime.datetime):
|
|
20
|
+
r = o.isoformat()
|
|
21
|
+
if o.microsecond:
|
|
22
|
+
r = r[:23] + r[26:]
|
|
23
|
+
if r.endswith("+00:00"):
|
|
24
|
+
r = r.removesuffix("+00:00") + "Z"
|
|
25
|
+
return r
|
|
26
|
+
elif isinstance(o, datetime.date):
|
|
27
|
+
return o.isoformat()
|
|
28
|
+
elif isinstance(o, datetime.time):
|
|
29
|
+
if is_aware(o):
|
|
30
|
+
raise ValueError("JSON can't represent timezone-aware times.")
|
|
31
|
+
r = o.isoformat()
|
|
32
|
+
if o.microsecond:
|
|
33
|
+
r = r[:12]
|
|
34
|
+
return r
|
|
35
|
+
elif isinstance(o, datetime.timedelta):
|
|
36
|
+
return duration_iso_string(o)
|
|
37
|
+
elif isinstance(o, decimal.Decimal | uuid.UUID | Promise):
|
|
38
|
+
return str(o)
|
|
39
|
+
else:
|
|
40
|
+
return super().default(o)
|