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/logs/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Logging
2
+
3
+ Default logging settings and key-value logger.
4
+
5
+ In Python, logging can be a surprisingly complex topic.
6
+
7
+ So Plain aims for easy-to-use defaults that "just work".
8
+
9
+ ## `app_logger`
10
+
11
+ The default `app_logger` doesn't do much!
12
+
13
+ But it is paired with the default [settings](#) to actually show the logs like you would expect,
14
+ without any additional configuration.
15
+
16
+ ```python
17
+ from plain.logs import app_logger
18
+
19
+
20
+ def example_function():
21
+ app_logger.info("Hey!")
22
+ ```
23
+
24
+ ## `app_logger.kv`
plain/logs/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .configure import configure_logging
2
+ from .loggers import app_logger
3
+ from .utils import log_response
4
+
5
+ __all__ = ["app_logger", "log_response", "configure_logging"]
@@ -0,0 +1,39 @@
1
+ import logging
2
+ import logging.config
3
+ from os import environ
4
+
5
+
6
+ def configure_logging(logging_settings):
7
+ # Load the defaults
8
+ default_logging = {
9
+ "version": 1,
10
+ "disable_existing_loggers": False,
11
+ "formatters": {
12
+ "simple": {
13
+ "format": "[%(levelname)s] %(message)s",
14
+ },
15
+ },
16
+ "handlers": {
17
+ "console": {
18
+ "level": "INFO",
19
+ "class": "logging.StreamHandler",
20
+ "formatter": "simple",
21
+ },
22
+ },
23
+ "loggers": {
24
+ "plain": {
25
+ "handlers": ["console"],
26
+ "level": environ.get("PLAIN_LOG_LEVEL", "INFO"),
27
+ },
28
+ "app": {
29
+ "handlers": ["console"],
30
+ "level": environ.get("APP_LOG_LEVEL", "INFO"),
31
+ "propagate": False,
32
+ },
33
+ },
34
+ }
35
+ logging.config.dictConfig(default_logging)
36
+
37
+ # Then customize it from settings
38
+ if logging_settings:
39
+ logging.config.dictConfig(logging_settings)
plain/logs/loggers.py ADDED
@@ -0,0 +1,74 @@
1
+ import logging
2
+
3
+ app_logger = logging.getLogger("app")
4
+
5
+
6
+ class KVLogger:
7
+ def __init__(self, logger):
8
+ self.logger = logger
9
+ self.context = {} # A dict that will be output in every log message
10
+
11
+ def log(self, level, message, **kwargs):
12
+ msg_kwargs = {
13
+ **kwargs,
14
+ **self.context, # Put these last so they're at the end of the line
15
+ }
16
+ self.logger.log(level, f"{message} {self._format_kwargs(msg_kwargs)}")
17
+
18
+ def _format_kwargs(self, kwargs):
19
+ outputs = []
20
+
21
+ for k, v in kwargs.items():
22
+ self._validate_key(k)
23
+ formatted_value = self._format_value(v)
24
+ outputs.append(f"{k}={formatted_value}")
25
+
26
+ return " ".join(outputs)
27
+
28
+ def _validate_key(self, key):
29
+ if " " in key:
30
+ raise ValueError("Keys cannot have spaces")
31
+
32
+ if "=" in key:
33
+ raise ValueError("Keys cannot have equals signs")
34
+
35
+ if '"' in key or "'" in key:
36
+ raise ValueError("Keys cannot have quotes")
37
+
38
+ def _format_value(self, value):
39
+ if isinstance(value, str):
40
+ s = value
41
+ else:
42
+ s = str(value)
43
+
44
+ if '"' in s:
45
+ # Escape quotes and surround it
46
+ s = s.replace('"', '\\"')
47
+ s = f'"{s}"'
48
+ elif s == "":
49
+ # Quote empty strings instead of printing nothing
50
+ s = '""'
51
+ elif any(char in s for char in [" ", "/", "'", ":", "=", "."]):
52
+ # Surround these with quotes for parsers
53
+ s = f'"{s}"'
54
+
55
+ return s
56
+
57
+ def info(self, message, **kwargs):
58
+ self.log(logging.INFO, message, **kwargs)
59
+
60
+ def debug(self, message, **kwargs):
61
+ self.log(logging.DEBUG, message, **kwargs)
62
+
63
+ def warning(self, message, **kwargs):
64
+ self.log(logging.WARNING, message, **kwargs)
65
+
66
+ def error(self, message, **kwargs):
67
+ self.log(logging.ERROR, message, **kwargs)
68
+
69
+ def critical(self, message, **kwargs):
70
+ self.log(logging.CRITICAL, message, **kwargs)
71
+
72
+
73
+ # Make this accessible from the app_logger
74
+ app_logger.kv = KVLogger(app_logger)
plain/logs/utils.py ADDED
@@ -0,0 +1,46 @@
1
+ import logging
2
+
3
+ request_logger = logging.getLogger("plain.request")
4
+
5
+
6
+ def log_response(
7
+ message,
8
+ *args,
9
+ response=None,
10
+ request=None,
11
+ logger=request_logger,
12
+ level=None,
13
+ exception=None,
14
+ ):
15
+ """
16
+ Log errors based on Response status.
17
+
18
+ Log 5xx responses as errors and 4xx responses as warnings (unless a level
19
+ is given as a keyword argument). The Response status_code and the
20
+ request are passed to the logger's extra parameter.
21
+ """
22
+ # Check if the response has already been logged. Multiple requests to log
23
+ # the same response can be received in some cases, e.g., when the
24
+ # response is the result of an exception and is logged when the exception
25
+ # is caught, to record the exception.
26
+ if getattr(response, "_has_been_logged", False):
27
+ return
28
+
29
+ if level is None:
30
+ if response.status_code >= 500:
31
+ level = "error"
32
+ elif response.status_code >= 400:
33
+ level = "warning"
34
+ else:
35
+ level = "info"
36
+
37
+ getattr(logger, level)(
38
+ message,
39
+ *args,
40
+ extra={
41
+ "status_code": response.status_code,
42
+ "request": request,
43
+ },
44
+ exc_info=exception,
45
+ )
46
+ response._has_been_logged = True
@@ -0,0 +1,3 @@
1
+ # Middleware
2
+
3
+ Hook into the request/response cycle.
File without changes
@@ -0,0 +1,52 @@
1
+ """
2
+ Clickjacking Protection Middleware.
3
+
4
+ This module provides a middleware that implements protection against a
5
+ malicious site loading resources from your site in a hidden frame.
6
+ """
7
+
8
+ from plain.runtime import settings
9
+
10
+
11
+ class XFrameOptionsMiddleware:
12
+ """
13
+ Set the X-Frame-Options HTTP header in HTTP responses.
14
+
15
+ Do not set the header if it's already set or if the response contains
16
+ a xframe_options_exempt value set to True.
17
+
18
+ By default, set the X-Frame-Options header to 'DENY', meaning the response
19
+ cannot be displayed in a frame, regardless of the site attempting to do so.
20
+ To enable the response to be loaded on a frame within the same site, set
21
+ X_FRAME_OPTIONS in your project's Plain settings to 'SAMEORIGIN'.
22
+ """
23
+
24
+ def __init__(self, get_response):
25
+ self.get_response = get_response
26
+
27
+ def __call__(self, request):
28
+ response = self.get_response(request)
29
+
30
+ # Don't set it if it's already in the response
31
+ if response.get("X-Frame-Options") is not None:
32
+ return response
33
+
34
+ # Don't set it if they used @xframe_options_exempt
35
+ if getattr(response, "xframe_options_exempt", False):
36
+ return response
37
+
38
+ response.headers["X-Frame-Options"] = self.get_xframe_options_value(
39
+ request,
40
+ response,
41
+ )
42
+ return response
43
+
44
+ def get_xframe_options_value(self, request, response):
45
+ """
46
+ Get the value to set for the X_FRAME_OPTIONS header. Use the value from
47
+ the X_FRAME_OPTIONS setting, or 'DENY' if not set.
48
+
49
+ This method can be overridden if needed, allowing it to vary based on
50
+ the request or response.
51
+ """
52
+ return getattr(settings, "X_FRAME_OPTIONS", "DENY").upper()
@@ -0,0 +1,87 @@
1
+ from plain.http import ResponsePermanentRedirect
2
+ from plain.runtime import settings
3
+ from plain.urls import is_valid_path
4
+ from plain.utils.http import escape_leading_slashes
5
+
6
+
7
+ class CommonMiddleware:
8
+ """
9
+ "Common" middleware for taking care of some basic operations:
10
+
11
+ - URL rewriting: Based on the APPEND_SLASH setting,
12
+ append missing slashes.
13
+
14
+ - If APPEND_SLASH is set and the initial URL doesn't end with a
15
+ slash, and it is not found in urlpatterns, form a new URL by
16
+ appending a slash at the end. If this new URL is found in
17
+ urlpatterns, return an HTTP redirect to this new URL; otherwise
18
+ process the initial URL as usual.
19
+
20
+ This behavior can be customized by subclassing CommonMiddleware and
21
+ overriding the response_redirect_class attribute.
22
+ """
23
+
24
+ response_redirect_class = ResponsePermanentRedirect
25
+
26
+ def __init__(self, get_response):
27
+ self.get_response = get_response
28
+
29
+ def __call__(self, request):
30
+ """
31
+ Rewrite the URL based on settings.APPEND_SLASH
32
+ """
33
+
34
+ response = self.get_response(request)
35
+
36
+ """
37
+ When the status code of the response is 404, it may redirect to a path
38
+ with an appended slash if should_redirect_with_slash() returns True.
39
+ """
40
+ # If the given URL is "Not Found", then check if we should redirect to
41
+ # a path with a slash appended.
42
+ if response.status_code == 404 and self.should_redirect_with_slash(request):
43
+ return self.response_redirect_class(self.get_full_path_with_slash(request))
44
+
45
+ # Add the Content-Length header to non-streaming responses if not
46
+ # already set.
47
+ if not response.streaming and not response.has_header("Content-Length"):
48
+ response.headers["Content-Length"] = str(len(response.content))
49
+
50
+ return response
51
+
52
+ def should_redirect_with_slash(self, request):
53
+ """
54
+ Return True if settings.APPEND_SLASH is True and appending a slash to
55
+ the request path turns an invalid path into a valid one.
56
+ """
57
+ if settings.APPEND_SLASH and not request.path_info.endswith("/"):
58
+ urlconf = getattr(request, "urlconf", None)
59
+ if not is_valid_path(request.path_info, urlconf):
60
+ match = is_valid_path("%s/" % request.path_info, urlconf)
61
+ if match:
62
+ view = match.func
63
+ return getattr(view, "should_append_slash", True)
64
+ return False
65
+
66
+ def get_full_path_with_slash(self, request):
67
+ """
68
+ Return the full path of the request with a trailing slash appended.
69
+
70
+ Raise a RuntimeError if settings.DEBUG is True and request.method is
71
+ POST, PUT, or PATCH.
72
+ """
73
+ new_path = request.get_full_path(force_append_slash=True)
74
+ # Prevent construction of scheme relative urls.
75
+ new_path = escape_leading_slashes(new_path)
76
+ if settings.DEBUG and request.method in ("POST", "PUT", "PATCH"):
77
+ raise RuntimeError(
78
+ "You called this URL via {method}, but the URL doesn't end "
79
+ "in a slash and you have APPEND_SLASH set. Plain can't "
80
+ "redirect to the slash URL while maintaining {method} data. "
81
+ "Change your form to point to {url} (note the trailing "
82
+ "slash), or set APPEND_SLASH=False in your Plain settings.".format(
83
+ method=request.method,
84
+ url=request.get_host() + new_path,
85
+ )
86
+ )
87
+ return new_path
@@ -0,0 +1,64 @@
1
+ from plain.utils.cache import patch_vary_headers
2
+ from plain.utils.regex_helper import _lazy_re_compile
3
+ from plain.utils.text import compress_sequence, compress_string
4
+
5
+ re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")
6
+
7
+
8
+ class GZipMiddleware:
9
+ """
10
+ Compress content if the browser allows gzip compression.
11
+ Set the Vary header accordingly, so that caches will base their storage
12
+ on the Accept-Encoding header.
13
+ """
14
+
15
+ max_random_bytes = 100
16
+
17
+ def __init__(self, get_response):
18
+ self.get_response = get_response
19
+
20
+ def __call__(self, request):
21
+ response = self.get_response(request)
22
+
23
+ # It's not worth attempting to compress really short responses.
24
+ if not response.streaming and len(response.content) < 200:
25
+ return response
26
+
27
+ # Avoid gzipping if we've already got a content-encoding.
28
+ if response.has_header("Content-Encoding"):
29
+ return response
30
+
31
+ patch_vary_headers(response, ("Accept-Encoding",))
32
+
33
+ ae = request.META.get("HTTP_ACCEPT_ENCODING", "")
34
+ if not re_accepts_gzip.search(ae):
35
+ return response
36
+
37
+ if response.streaming:
38
+ response.streaming_content = compress_sequence(
39
+ response.streaming_content,
40
+ max_random_bytes=self.max_random_bytes,
41
+ )
42
+ # Delete the `Content-Length` header for streaming content, because
43
+ # we won't know the compressed size until we stream it.
44
+ del response.headers["Content-Length"]
45
+ else:
46
+ # Return the compressed content only if it's actually shorter.
47
+ compressed_content = compress_string(
48
+ response.content,
49
+ max_random_bytes=self.max_random_bytes,
50
+ )
51
+ if len(compressed_content) >= len(response.content):
52
+ return response
53
+ response.content = compressed_content
54
+ response.headers["Content-Length"] = str(len(response.content))
55
+
56
+ # If there is a strong ETag, make it weak to fulfill the requirements
57
+ # of RFC 9110 Section 8.8.1 while also allowing conditional request
58
+ # matches on ETags.
59
+ etag = response.get("ETag")
60
+ if etag and etag.startswith('"'):
61
+ response.headers["ETag"] = "W/" + etag
62
+ response.headers["Content-Encoding"] = "gzip"
63
+
64
+ return response
@@ -0,0 +1,64 @@
1
+ import re
2
+
3
+ from plain.http import ResponsePermanentRedirect
4
+ from plain.runtime import settings
5
+
6
+
7
+ class SecurityMiddleware:
8
+ def __init__(self, get_response):
9
+ self.get_response = get_response
10
+ self.sts_seconds = settings.SECURE_HSTS_SECONDS
11
+ self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
12
+ self.sts_preload = settings.SECURE_HSTS_PRELOAD
13
+ self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
14
+ self.redirect = settings.SECURE_SSL_REDIRECT
15
+ self.redirect_host = settings.SECURE_SSL_HOST
16
+ self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
17
+ self.referrer_policy = settings.SECURE_REFERRER_POLICY
18
+ self.cross_origin_opener_policy = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
19
+
20
+ def __call__(self, request):
21
+ path = request.path.lstrip("/")
22
+ if (
23
+ self.redirect
24
+ and not request.is_secure()
25
+ and not any(pattern.search(path) for pattern in self.redirect_exempt)
26
+ ):
27
+ host = self.redirect_host or request.get_host()
28
+ return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
29
+
30
+ response = self.get_response(request)
31
+
32
+ if (
33
+ self.sts_seconds
34
+ and request.is_secure()
35
+ and "Strict-Transport-Security" not in response
36
+ ):
37
+ sts_header = "max-age=%s" % self.sts_seconds
38
+ if self.sts_include_subdomains:
39
+ sts_header += "; includeSubDomains"
40
+ if self.sts_preload:
41
+ sts_header += "; preload"
42
+ response.headers["Strict-Transport-Security"] = sts_header
43
+
44
+ if self.content_type_nosniff:
45
+ response.headers.setdefault("X-Content-Type-Options", "nosniff")
46
+
47
+ if self.referrer_policy:
48
+ # Support a comma-separated string or iterable of values to allow
49
+ # fallback.
50
+ response.headers.setdefault(
51
+ "Referrer-Policy",
52
+ ",".join(
53
+ [v.strip() for v in self.referrer_policy.split(",")]
54
+ if isinstance(self.referrer_policy, str)
55
+ else self.referrer_policy
56
+ ),
57
+ )
58
+
59
+ if self.cross_origin_opener_policy:
60
+ response.setdefault(
61
+ "Cross-Origin-Opener-Policy",
62
+ self.cross_origin_opener_policy,
63
+ )
64
+ return response
@@ -0,0 +1,41 @@
1
+ # Packages
2
+
3
+ Create app-packages and install third-party packages from PyPI.
4
+
5
+ Like Django, Plain is heavily dependent on the concept of "packages".
6
+
7
+ In your `settings.py`, you define which packages are enabled with the `INSTALLED_PACKAGES` setting.
8
+
9
+ ```python
10
+ # app/settings.py
11
+ INSTALLED_PACKAGES = [
12
+
13
+ ]
14
+ ```
15
+
16
+ These can refer to third-party packages from PyPI (after you've installed them with `pip`),
17
+ or they can refer to packages that you've written yourself.
18
+
19
+ - some packages don't need to be installed as packages, but most do (and should specify)
20
+
21
+
22
+ ## Your own packages
23
+
24
+ - naming examples
25
+ - startapp
26
+
27
+
28
+ ## App settings
29
+
30
+ ```python
31
+ # <app>/default_settings.py
32
+ EXAMPLE_SETTING: str = "example"
33
+ ```
34
+
35
+ ```python
36
+ # <app>/models.py
37
+ from plain.runtime import settings
38
+
39
+
40
+ print(settings.EXAMPLE_SETTING)
41
+ ```
@@ -0,0 +1,4 @@
1
+ from .config import PackageConfig
2
+ from .registry import packages
3
+
4
+ __all__ = ["PackageConfig", "packages"]