plain 0.68.0__py3-none-any.whl → 0.101.2__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 (195) hide show
  1. plain/CHANGELOG.md +656 -1
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -36
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +110 -26
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +27 -75
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +96 -10
  27. plain/cli/{agent/request.py → request.py} +67 -33
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -8
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +27 -16
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +209 -24
  84. plain/preflight/__init__.py +1 -0
  85. plain/preflight/checks.py +3 -1
  86. plain/preflight/files.py +3 -1
  87. plain/preflight/registry.py +26 -11
  88. plain/preflight/results.py +15 -7
  89. plain/preflight/security.py +15 -13
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +4 -1
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +34 -25
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +13 -5
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +38 -22
  145. plain/urls/resolvers.py +35 -25
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/csrf/views.py +0 -31
  191. plain/logs/utils.py +0 -46
  192. plain/templates/AGENTS.md +0 -3
  193. plain-0.68.0.dist-info/RECORD +0 -169
  194. plain-0.68.0.dist-info/entry_points.txt +0 -5
  195. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/http/__init__.py CHANGED
@@ -1,49 +1,64 @@
1
- from plain.http.cookie import parse_cookie
2
- from plain.http.request import (
3
- HttpHeaders,
4
- HttpRequest,
1
+ from .cookie import parse_cookie
2
+ from .exceptions import (
3
+ BadRequestError400,
4
+ ForbiddenError403,
5
+ NotFoundError404,
6
+ RequestDataTooBigError400,
7
+ SuspiciousFileOperationError400,
8
+ SuspiciousMultipartFormError400,
9
+ SuspiciousOperationError400,
10
+ TooManyFieldsSentError400,
11
+ TooManyFilesSentError400,
12
+ )
13
+ from .middleware import HttpMiddleware
14
+ from .request import (
5
15
  QueryDict,
6
16
  RawPostDataException,
17
+ Request,
18
+ RequestHeaders,
7
19
  UnreadablePostError,
8
20
  )
9
- from plain.http.response import (
21
+ from .response import (
10
22
  BadHeaderError,
11
23
  FileResponse,
12
- Http404,
13
24
  JsonResponse,
25
+ NotAllowedResponse,
26
+ NotModifiedResponse,
27
+ RedirectResponse,
14
28
  Response,
15
- ResponseBadRequest,
16
29
  ResponseBase,
17
- ResponseForbidden,
18
- ResponseGone,
19
- ResponseNotAllowed,
20
- ResponseNotFound,
21
- ResponseNotModified,
22
- ResponseRedirect,
23
- ResponseServerError,
24
30
  StreamingResponse,
25
31
  )
26
32
 
27
33
  __all__ = [
34
+ # Middleware
35
+ "HttpMiddleware",
36
+ # Cookies
28
37
  "parse_cookie",
29
- "HttpHeaders",
30
- "HttpRequest",
38
+ # Request
39
+ "Request",
40
+ "RequestHeaders",
31
41
  "QueryDict",
32
42
  "RawPostDataException",
33
43
  "UnreadablePostError",
44
+ # Response
34
45
  "Response",
35
46
  "ResponseBase",
36
47
  "StreamingResponse",
37
- "ResponseRedirect",
38
- "ResponseNotModified",
39
- "ResponseBadRequest",
40
- "ResponseForbidden",
41
- "ResponseNotFound",
42
- "ResponseNotAllowed",
43
- "ResponseGone",
44
- "ResponseServerError",
45
- "Http404",
46
- "BadHeaderError",
48
+ "RedirectResponse",
49
+ "NotModifiedResponse",
50
+ "NotAllowedResponse",
47
51
  "JsonResponse",
48
52
  "FileResponse",
53
+ "BadHeaderError",
54
+ # Exceptions
55
+ "NotFoundError404",
56
+ "ForbiddenError403",
57
+ "BadRequestError400",
58
+ "SuspiciousOperationError400",
59
+ "SuspiciousMultipartFormError400",
60
+ "SuspiciousFileOperationError400",
61
+ "TooManyFieldsSentError400",
62
+ "TooManyFilesSentError400",
63
+ "RequestDataTooBigError400",
49
64
  ]
plain/http/cookie.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from http import cookies
2
4
 
3
5
  from plain.runtime import settings
@@ -5,7 +7,7 @@ from plain.signing import BadSignature, TimestampSigner
5
7
  from plain.utils.encoding import force_bytes
6
8
 
7
9
 
8
- def parse_cookie(cookie):
10
+ def parse_cookie(cookie: str) -> dict[str, str]:
9
11
  """
10
12
  Return a dictionary parsed from a `Cookie:` header string.
11
13
  """
@@ -24,7 +26,7 @@ def parse_cookie(cookie):
24
26
  return cookiedict
25
27
 
26
28
 
27
- def _cookie_key(key):
29
+ def _cookie_key(key: str) -> bytes:
28
30
  """
29
31
  Generate a key for cookie signing that matches the pattern used by
30
32
  set_signed_cookie and get_signed_cookie.
@@ -32,19 +34,19 @@ def _cookie_key(key):
32
34
  return b"plain.http.cookies" + force_bytes(key)
33
35
 
34
36
 
35
- def get_signed_cookie_signer(key, salt=""):
37
+ def get_signed_cookie_signer(key: str, salt: str = "") -> TimestampSigner:
36
38
  """
37
39
  Create a TimestampSigner for signed cookies with the same configuration
38
40
  used by both set_signed_cookie and get_signed_cookie.
39
41
  """
40
42
  return TimestampSigner(
41
- key=_cookie_key(settings.SECRET_KEY),
42
- fallback_keys=map(_cookie_key, settings.SECRET_KEY_FALLBACKS),
43
+ key=_cookie_key(settings.SECRET_KEY).decode(),
44
+ fallback_keys=[_cookie_key(k).decode() for k in settings.SECRET_KEY_FALLBACKS],
43
45
  salt=key + salt,
44
46
  )
45
47
 
46
48
 
47
- def sign_cookie_value(key, value, salt=""):
49
+ def sign_cookie_value(key: str, value: str, salt: str = "") -> str:
48
50
  """
49
51
  Sign a cookie value using the standard Plain cookie signing approach.
50
52
  """
@@ -52,7 +54,13 @@ def sign_cookie_value(key, value, salt=""):
52
54
  return signer.sign(value)
53
55
 
54
56
 
55
- def unsign_cookie_value(key, signed_value, salt="", max_age=None, default=None):
57
+ def unsign_cookie_value(
58
+ key: str,
59
+ signed_value: str,
60
+ salt: str = "",
61
+ max_age: int | None = None,
62
+ default: str | None = None,
63
+ ) -> str | None:
56
64
  """
57
65
  Unsign a cookie value using the standard Plain cookie signing approach.
58
66
  Returns the default value if the signature is invalid or the cookie has expired.
@@ -0,0 +1,65 @@
1
+ """
2
+ HTTP exceptions that are converted to HTTP responses by the exception handler.
3
+ The suffix indicates the HTTP status code that will be returned.
4
+ """
5
+
6
+
7
+ class NotFoundError404(Exception):
8
+ """The requested resource was not found (HTTP 404)"""
9
+
10
+ pass
11
+
12
+
13
+ class ForbiddenError403(Exception):
14
+ """The user did not have permission to do that (HTTP 403)"""
15
+
16
+ pass
17
+
18
+
19
+ class BadRequestError400(Exception):
20
+ """The request is malformed and cannot be processed (HTTP 400)"""
21
+
22
+ pass
23
+
24
+
25
+ class SuspiciousOperationError400(Exception):
26
+ """The user did something suspicious (HTTP 400)"""
27
+
28
+
29
+ class SuspiciousMultipartFormError400(SuspiciousOperationError400):
30
+ """Suspect MIME request in multipart form data"""
31
+
32
+ pass
33
+
34
+
35
+ class SuspiciousFileOperationError400(SuspiciousOperationError400):
36
+ """A Suspicious filesystem operation was attempted"""
37
+
38
+ pass
39
+
40
+
41
+ class TooManyFieldsSentError400(SuspiciousOperationError400):
42
+ """
43
+ The number of fields in a GET or POST request exceeded
44
+ settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.
45
+ """
46
+
47
+ pass
48
+
49
+
50
+ class TooManyFilesSentError400(SuspiciousOperationError400):
51
+ """
52
+ The number of fields in a GET or POST request exceeded
53
+ settings.DATA_UPLOAD_MAX_NUMBER_FILES.
54
+ """
55
+
56
+ pass
57
+
58
+
59
+ class RequestDataTooBigError400(SuspiciousOperationError400):
60
+ """
61
+ The size of the request (excluding any file uploads) exceeded
62
+ settings.DATA_UPLOAD_MAX_MEMORY_SIZE.
63
+ """
64
+
65
+ pass
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from plain.http import Request, Response
9
+
10
+
11
+ class HttpMiddleware(ABC):
12
+ """
13
+ Abstract base class for HTTP middleware.
14
+
15
+ Subclasses must implement process_request() to handle the request/response cycle.
16
+
17
+ Example:
18
+ class MyMiddleware(HttpMiddleware):
19
+ def process_request(self, request: Request) -> Response:
20
+ # Pre-processing
21
+ response = self.get_response(request)
22
+ # Post-processing
23
+ return response
24
+ """
25
+
26
+ def __init__(self, get_response: Callable[[Request], Response]):
27
+ self.get_response = get_response
28
+
29
+ @abstractmethod
30
+ def process_request(self, request: Request) -> Response:
31
+ """Process the request and return a response. Must be implemented by subclasses."""
32
+ ...