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
File without changes
@@ -0,0 +1,3 @@
1
+ # Files
2
+
3
+ File upload handling and utilities.
@@ -0,0 +1,3 @@
1
+ from plain.internal.files.base import File
2
+
3
+ __all__ = ["File"]
@@ -0,0 +1,161 @@
1
+ import os
2
+ from io import BytesIO, StringIO, UnsupportedOperation
3
+
4
+ from plain.internal.files.utils import FileProxyMixin
5
+ from plain.utils.functional import cached_property
6
+
7
+
8
+ class File(FileProxyMixin):
9
+ DEFAULT_CHUNK_SIZE = 64 * 2**10
10
+
11
+ def __init__(self, file, name=None):
12
+ self.file = file
13
+ if name is None:
14
+ name = getattr(file, "name", None)
15
+ self.name = name
16
+ if hasattr(file, "mode"):
17
+ self.mode = file.mode
18
+
19
+ def __str__(self):
20
+ return self.name or ""
21
+
22
+ def __repr__(self):
23
+ return "<{}: {}>".format(self.__class__.__name__, self or "None")
24
+
25
+ def __bool__(self):
26
+ return bool(self.name)
27
+
28
+ def __len__(self):
29
+ return self.size
30
+
31
+ @cached_property
32
+ def size(self):
33
+ if hasattr(self.file, "size"):
34
+ return self.file.size
35
+ if hasattr(self.file, "name"):
36
+ try:
37
+ return os.path.getsize(self.file.name)
38
+ except (OSError, TypeError):
39
+ pass
40
+ if hasattr(self.file, "tell") and hasattr(self.file, "seek"):
41
+ pos = self.file.tell()
42
+ self.file.seek(0, os.SEEK_END)
43
+ size = self.file.tell()
44
+ self.file.seek(pos)
45
+ return size
46
+ raise AttributeError("Unable to determine the file's size.")
47
+
48
+ def chunks(self, chunk_size=None):
49
+ """
50
+ Read the file and yield chunks of ``chunk_size`` bytes (defaults to
51
+ ``File.DEFAULT_CHUNK_SIZE``).
52
+ """
53
+ chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
54
+ try:
55
+ self.seek(0)
56
+ except (AttributeError, UnsupportedOperation):
57
+ pass
58
+
59
+ while True:
60
+ data = self.read(chunk_size)
61
+ if not data:
62
+ break
63
+ yield data
64
+
65
+ def multiple_chunks(self, chunk_size=None):
66
+ """
67
+ Return ``True`` if you can expect multiple chunks.
68
+
69
+ NB: If a particular file representation is in memory, subclasses should
70
+ always return ``False`` -- there's no good reason to read from memory in
71
+ chunks.
72
+ """
73
+ return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)
74
+
75
+ def __iter__(self):
76
+ # Iterate over this file-like object by newlines
77
+ buffer_ = None
78
+ for chunk in self.chunks():
79
+ for line in chunk.splitlines(True):
80
+ if buffer_:
81
+ if endswith_cr(buffer_) and not equals_lf(line):
82
+ # Line split after a \r newline; yield buffer_.
83
+ yield buffer_
84
+ # Continue with line.
85
+ else:
86
+ # Line either split without a newline (line
87
+ # continues after buffer_) or with \r\n
88
+ # newline (line == b'\n').
89
+ line = buffer_ + line
90
+ # buffer_ handled, clear it.
91
+ buffer_ = None
92
+
93
+ # If this is the end of a \n or \r\n line, yield.
94
+ if endswith_lf(line):
95
+ yield line
96
+ else:
97
+ buffer_ = line
98
+
99
+ if buffer_ is not None:
100
+ yield buffer_
101
+
102
+ def __enter__(self):
103
+ return self
104
+
105
+ def __exit__(self, exc_type, exc_value, tb):
106
+ self.close()
107
+
108
+ def open(self, mode=None):
109
+ if not self.closed:
110
+ self.seek(0)
111
+ elif self.name and os.path.exists(self.name):
112
+ self.file = open(self.name, mode or self.mode)
113
+ else:
114
+ raise ValueError("The file cannot be reopened.")
115
+ return self
116
+
117
+ def close(self):
118
+ self.file.close()
119
+
120
+
121
+ class ContentFile(File):
122
+ """
123
+ A File-like object that takes just raw content, rather than an actual file.
124
+ """
125
+
126
+ def __init__(self, content, name=None):
127
+ stream_class = StringIO if isinstance(content, str) else BytesIO
128
+ super().__init__(stream_class(content), name=name)
129
+ self.size = len(content)
130
+
131
+ def __str__(self):
132
+ return "Raw content"
133
+
134
+ def __bool__(self):
135
+ return True
136
+
137
+ def open(self, mode=None):
138
+ self.seek(0)
139
+ return self
140
+
141
+ def close(self):
142
+ pass
143
+
144
+ def write(self, data):
145
+ self.__dict__.pop("size", None) # Clear the computed size.
146
+ return self.file.write(data)
147
+
148
+
149
+ def endswith_cr(line):
150
+ """Return True if line (a text or bytestring) ends with '\r'."""
151
+ return line.endswith("\r" if isinstance(line, str) else b"\r")
152
+
153
+
154
+ def endswith_lf(line):
155
+ """Return True if line (a text or bytestring) ends with '\n'."""
156
+ return line.endswith("\n" if isinstance(line, str) else b"\n")
157
+
158
+
159
+ def equals_lf(line):
160
+ """Return True if line (a text or bytestring) equals '\n'."""
161
+ return line == ("\n" if isinstance(line, str) else b"\n")
@@ -0,0 +1,127 @@
1
+ """
2
+ Portable file locking utilities.
3
+
4
+ Based partially on an example by Jonathan Feignberg in the Python
5
+ Cookbook [1] (licensed under the Python Software License) and a ctypes port by
6
+ Anatoly Techtonik for Roundup [2] (license [3]).
7
+
8
+ [1] https://code.activestate.com/recipes/65203/
9
+ [2] https://sourceforge.net/p/roundup/code/ci/default/tree/roundup/backends/portalocker.py # NOQA
10
+ [3] https://sourceforge.net/p/roundup/code/ci/default/tree/COPYING.txt
11
+
12
+ Example Usage::
13
+
14
+ >>> from plain.internal.files import locks
15
+ >>> with open('./file', 'wb') as f:
16
+ ... locks.lock(f, locks.LOCK_EX)
17
+ ... f.write('Plain')
18
+ """
19
+ import os
20
+
21
+ __all__ = ("LOCK_EX", "LOCK_SH", "LOCK_NB", "lock", "unlock")
22
+
23
+
24
+ def _fd(f):
25
+ """Get a filedescriptor from something which could be a file or an fd."""
26
+ return f.fileno() if hasattr(f, "fileno") else f
27
+
28
+
29
+ if os.name == "nt":
30
+ import msvcrt
31
+ from ctypes import (
32
+ POINTER,
33
+ Structure,
34
+ Union,
35
+ WinDLL,
36
+ byref,
37
+ c_int64,
38
+ c_ulong,
39
+ c_void_p,
40
+ sizeof,
41
+ )
42
+ from ctypes.wintypes import BOOL, DWORD, HANDLE
43
+
44
+ LOCK_SH = 0 # the default
45
+ LOCK_NB = 0x1 # LOCKFILE_FAIL_IMMEDIATELY
46
+ LOCK_EX = 0x2 # LOCKFILE_EXCLUSIVE_LOCK
47
+
48
+ # --- Adapted from the pyserial project ---
49
+ # detect size of ULONG_PTR
50
+ if sizeof(c_ulong) != sizeof(c_void_p):
51
+ ULONG_PTR = c_int64
52
+ else:
53
+ ULONG_PTR = c_ulong
54
+ PVOID = c_void_p
55
+
56
+ # --- Union inside Structure by stackoverflow:3480240 ---
57
+ class _OFFSET(Structure):
58
+ _fields_ = [("Offset", DWORD), ("OffsetHigh", DWORD)]
59
+
60
+ class _OFFSET_UNION(Union):
61
+ _anonymous_ = ["_offset"]
62
+ _fields_ = [("_offset", _OFFSET), ("Pointer", PVOID)]
63
+
64
+ class OVERLAPPED(Structure):
65
+ _anonymous_ = ["_offset_union"]
66
+ _fields_ = [
67
+ ("Internal", ULONG_PTR),
68
+ ("InternalHigh", ULONG_PTR),
69
+ ("_offset_union", _OFFSET_UNION),
70
+ ("hEvent", HANDLE),
71
+ ]
72
+
73
+ LPOVERLAPPED = POINTER(OVERLAPPED)
74
+
75
+ # --- Define function prototypes for extra safety ---
76
+ kernel32 = WinDLL("kernel32")
77
+ LockFileEx = kernel32.LockFileEx
78
+ LockFileEx.restype = BOOL
79
+ LockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, DWORD, LPOVERLAPPED]
80
+ UnlockFileEx = kernel32.UnlockFileEx
81
+ UnlockFileEx.restype = BOOL
82
+ UnlockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, LPOVERLAPPED]
83
+
84
+ def lock(f, flags):
85
+ hfile = msvcrt.get_osfhandle(_fd(f))
86
+ overlapped = OVERLAPPED()
87
+ ret = LockFileEx(hfile, flags, 0, 0, 0xFFFF0000, byref(overlapped))
88
+ return bool(ret)
89
+
90
+ def unlock(f):
91
+ hfile = msvcrt.get_osfhandle(_fd(f))
92
+ overlapped = OVERLAPPED()
93
+ ret = UnlockFileEx(hfile, 0, 0, 0xFFFF0000, byref(overlapped))
94
+ return bool(ret)
95
+
96
+ else:
97
+ try:
98
+ import fcntl
99
+
100
+ LOCK_SH = fcntl.LOCK_SH # shared lock
101
+ LOCK_NB = fcntl.LOCK_NB # non-blocking
102
+ LOCK_EX = fcntl.LOCK_EX
103
+ except (ImportError, AttributeError):
104
+ # File locking is not supported.
105
+ LOCK_EX = LOCK_SH = LOCK_NB = 0
106
+
107
+ # Dummy functions that don't do anything.
108
+ def lock(f, flags):
109
+ # File is not locked
110
+ return False
111
+
112
+ def unlock(f):
113
+ # File is unlocked
114
+ return True
115
+
116
+ else:
117
+
118
+ def lock(f, flags):
119
+ try:
120
+ fcntl.flock(_fd(f), flags)
121
+ return True
122
+ except BlockingIOError:
123
+ return False
124
+
125
+ def unlock(f):
126
+ fcntl.flock(_fd(f), fcntl.LOCK_UN)
127
+ return True
@@ -0,0 +1,102 @@
1
+ """
2
+ Move a file in the safest way possible::
3
+
4
+ >>> from plain.internal.files.move import file_move_safe
5
+ >>> file_move_safe("/tmp/old_file", "/tmp/new_file")
6
+ """
7
+
8
+ import os
9
+ from shutil import copymode, copystat
10
+
11
+ from plain.internal.files import locks
12
+
13
+ __all__ = ["file_move_safe"]
14
+
15
+
16
+ def _samefile(src, dst):
17
+ # Macintosh, Unix.
18
+ if hasattr(os.path, "samefile"):
19
+ try:
20
+ return os.path.samefile(src, dst)
21
+ except OSError:
22
+ return False
23
+
24
+ # All other platforms: check for same pathname.
25
+ return os.path.normcase(os.path.abspath(src)) == os.path.normcase(
26
+ os.path.abspath(dst)
27
+ )
28
+
29
+
30
+ def file_move_safe(
31
+ old_file_name, new_file_name, chunk_size=1024 * 64, allow_overwrite=False
32
+ ):
33
+ """
34
+ Move a file from one location to another in the safest way possible.
35
+
36
+ First, try ``os.rename``, which is simple but will break across filesystems.
37
+ If that fails, stream manually from one file to another in pure Python.
38
+
39
+ If the destination file exists and ``allow_overwrite`` is ``False``, raise
40
+ ``FileExistsError``.
41
+ """
42
+ # There's no reason to move if we don't have to.
43
+ if _samefile(old_file_name, new_file_name):
44
+ return
45
+
46
+ try:
47
+ if not allow_overwrite and os.access(new_file_name, os.F_OK):
48
+ raise FileExistsError(
49
+ "Destination file %s exists and allow_overwrite is False."
50
+ % new_file_name
51
+ )
52
+
53
+ os.rename(old_file_name, new_file_name)
54
+ return
55
+ except OSError:
56
+ # OSError happens with os.rename() if moving to another filesystem or
57
+ # when moving opened files on certain operating systems.
58
+ pass
59
+
60
+ # first open the old file, so that it won't go away
61
+ with open(old_file_name, "rb") as old_file:
62
+ # now open the new file, not forgetting allow_overwrite
63
+ fd = os.open(
64
+ new_file_name,
65
+ (
66
+ os.O_WRONLY
67
+ | os.O_CREAT
68
+ | getattr(os, "O_BINARY", 0)
69
+ | (os.O_EXCL if not allow_overwrite else 0)
70
+ ),
71
+ )
72
+ try:
73
+ locks.lock(fd, locks.LOCK_EX)
74
+ current_chunk = None
75
+ while current_chunk != b"":
76
+ current_chunk = old_file.read(chunk_size)
77
+ os.write(fd, current_chunk)
78
+ finally:
79
+ locks.unlock(fd)
80
+ os.close(fd)
81
+
82
+ try:
83
+ copystat(old_file_name, new_file_name)
84
+ except PermissionError:
85
+ # Certain filesystems (e.g. CIFS) fail to copy the file's metadata if
86
+ # the type of the destination filesystem isn't the same as the source
87
+ # filesystem. This also happens with some SELinux-enabled systems.
88
+ # Ignore that, but try to set basic permissions.
89
+ try:
90
+ copymode(old_file_name, new_file_name)
91
+ except PermissionError:
92
+ pass
93
+
94
+ try:
95
+ os.remove(old_file_name)
96
+ except PermissionError as e:
97
+ # Certain operating systems (Cygwin and Windows)
98
+ # fail when deleting opened files, ignore it. (For the
99
+ # systems where this happens, temporary files will be auto-deleted
100
+ # on close anyway.)
101
+ if getattr(e, "winerror", 0) != 32:
102
+ raise
@@ -0,0 +1,79 @@
1
+ """
2
+ The temp module provides a NamedTemporaryFile that can be reopened in the same
3
+ process on any platform. Most platforms use the standard Python
4
+ tempfile.NamedTemporaryFile class, but Windows users are given a custom class.
5
+
6
+ This is needed because the Python implementation of NamedTemporaryFile uses the
7
+ O_TEMPORARY flag under Windows, which prevents the file from being reopened
8
+ if the same flag is not provided [1][2]. Note that this does not address the
9
+ more general issue of opening a file for writing and reading in multiple
10
+ processes in a manner that works across platforms.
11
+
12
+ The custom version of NamedTemporaryFile doesn't support the same keyword
13
+ arguments available in tempfile.NamedTemporaryFile.
14
+
15
+ 1: https://mail.python.org/pipermail/python-list/2005-December/336957.html
16
+ 2: https://bugs.python.org/issue14243
17
+ """
18
+
19
+ import os
20
+ import tempfile
21
+
22
+ from plain.internal.files.utils import FileProxyMixin
23
+
24
+ __all__ = (
25
+ "NamedTemporaryFile",
26
+ "gettempdir",
27
+ )
28
+
29
+
30
+ if os.name == "nt":
31
+
32
+ class TemporaryFile(FileProxyMixin):
33
+ """
34
+ Temporary file object constructor that supports reopening of the
35
+ temporary file in Windows.
36
+
37
+ Unlike tempfile.NamedTemporaryFile from the standard library,
38
+ __init__() doesn't support the 'delete', 'buffering', 'encoding', or
39
+ 'newline' keyword arguments.
40
+ """
41
+
42
+ def __init__(self, mode="w+b", bufsize=-1, suffix="", prefix="", dir=None):
43
+ fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
44
+ self.name = name
45
+ self.file = os.fdopen(fd, mode, bufsize)
46
+ self.close_called = False
47
+
48
+ # Because close can be called during shutdown
49
+ # we need to cache os.unlink and access it
50
+ # as self.unlink only
51
+ unlink = os.unlink
52
+
53
+ def close(self):
54
+ if not self.close_called:
55
+ self.close_called = True
56
+ try:
57
+ self.file.close()
58
+ except OSError:
59
+ pass
60
+ try:
61
+ self.unlink(self.name)
62
+ except OSError:
63
+ pass
64
+
65
+ def __del__(self):
66
+ self.close()
67
+
68
+ def __enter__(self):
69
+ self.file.__enter__()
70
+ return self
71
+
72
+ def __exit__(self, exc, value, tb):
73
+ self.file.__exit__(exc, value, tb)
74
+
75
+ NamedTemporaryFile = TemporaryFile
76
+ else:
77
+ NamedTemporaryFile = tempfile.NamedTemporaryFile
78
+
79
+ gettempdir = tempfile.gettempdir
@@ -0,0 +1,150 @@
1
+ """
2
+ Classes representing uploaded files.
3
+ """
4
+
5
+ import os
6
+ from io import BytesIO
7
+
8
+ from plain.internal.files import temp as tempfile
9
+ from plain.internal.files.base import File
10
+ from plain.internal.files.utils import validate_file_name
11
+ from plain.runtime import settings
12
+
13
+ __all__ = (
14
+ "UploadedFile",
15
+ "TemporaryUploadedFile",
16
+ "InMemoryUploadedFile",
17
+ "SimpleUploadedFile",
18
+ )
19
+
20
+
21
+ class UploadedFile(File):
22
+ """
23
+ An abstract uploaded file (``TemporaryUploadedFile`` and
24
+ ``InMemoryUploadedFile`` are the built-in concrete subclasses).
25
+
26
+ An ``UploadedFile`` object behaves somewhat like a file object and
27
+ represents some file data that the user submitted with a form.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ file=None,
33
+ name=None,
34
+ content_type=None,
35
+ size=None,
36
+ charset=None,
37
+ content_type_extra=None,
38
+ ):
39
+ super().__init__(file, name)
40
+ self.size = size
41
+ self.content_type = content_type
42
+ self.charset = charset
43
+ self.content_type_extra = content_type_extra
44
+
45
+ def __repr__(self):
46
+ return f"<{self.__class__.__name__}: {self.name} ({self.content_type})>"
47
+
48
+ def _get_name(self):
49
+ return self._name
50
+
51
+ def _set_name(self, name):
52
+ # Sanitize the file name so that it can't be dangerous.
53
+ if name is not None:
54
+ # Just use the basename of the file -- anything else is dangerous.
55
+ name = os.path.basename(name)
56
+
57
+ # File names longer than 255 characters can cause problems on older OSes.
58
+ if len(name) > 255:
59
+ name, ext = os.path.splitext(name)
60
+ ext = ext[:255]
61
+ name = name[: 255 - len(ext)] + ext
62
+
63
+ name = validate_file_name(name)
64
+
65
+ self._name = name
66
+
67
+ name = property(_get_name, _set_name)
68
+
69
+
70
+ class TemporaryUploadedFile(UploadedFile):
71
+ """
72
+ A file uploaded to a temporary location (i.e. stream-to-disk).
73
+ """
74
+
75
+ def __init__(self, name, content_type, size, charset, content_type_extra=None):
76
+ _, ext = os.path.splitext(name)
77
+ file = tempfile.NamedTemporaryFile(
78
+ suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
79
+ )
80
+ super().__init__(file, name, content_type, size, charset, content_type_extra)
81
+
82
+ def temporary_file_path(self):
83
+ """Return the full path of this file."""
84
+ return self.file.name
85
+
86
+ def close(self):
87
+ try:
88
+ return self.file.close()
89
+ except FileNotFoundError:
90
+ # The file was moved or deleted before the tempfile could unlink
91
+ # it. Still sets self.file.close_called and calls
92
+ # self.file.file.close() before the exception.
93
+ pass
94
+
95
+
96
+ class InMemoryUploadedFile(UploadedFile):
97
+ """
98
+ A file uploaded into memory (i.e. stream-to-memory).
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ file,
104
+ field_name,
105
+ name,
106
+ content_type,
107
+ size,
108
+ charset,
109
+ content_type_extra=None,
110
+ ):
111
+ super().__init__(file, name, content_type, size, charset, content_type_extra)
112
+ self.field_name = field_name
113
+
114
+ def open(self, mode=None):
115
+ self.file.seek(0)
116
+ return self
117
+
118
+ def chunks(self, chunk_size=None):
119
+ self.file.seek(0)
120
+ yield self.read()
121
+
122
+ def multiple_chunks(self, chunk_size=None):
123
+ # Since it's in memory, we'll never have multiple chunks.
124
+ return False
125
+
126
+
127
+ class SimpleUploadedFile(InMemoryUploadedFile):
128
+ """
129
+ A simple representation of a file, which just has content, size, and a name.
130
+ """
131
+
132
+ def __init__(self, name, content, content_type="text/plain"):
133
+ content = content or b""
134
+ super().__init__(
135
+ BytesIO(content), None, name, content_type, len(content), None, None
136
+ )
137
+
138
+ @classmethod
139
+ def from_dict(cls, file_dict):
140
+ """
141
+ Create a SimpleUploadedFile object from a dictionary with keys:
142
+ - filename
143
+ - content-type
144
+ - content
145
+ """
146
+ return cls(
147
+ file_dict["filename"],
148
+ file_dict["content"],
149
+ file_dict.get("content-type", "text/plain"),
150
+ )