plain 0.66.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 (197) hide show
  1. plain/CHANGELOG.md +684 -0
  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 -53
  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 +112 -28
  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 +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  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 -13
  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 +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  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 +14 -27
  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 +56 -40
  145. plain/urls/resolvers.py +38 -28
  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.66.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.66.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/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/test/README.md CHANGED
@@ -1,42 +1,204 @@
1
- # Test
1
+ # plain.test
2
2
 
3
- **Testing utilities for Plain.**
3
+ **Testing utilities for making HTTP requests and inspecting responses.**
4
4
 
5
5
  - [Overview](#overview)
6
+ - [Making requests](#making-requests)
7
+ - [GET requests](#get-requests)
8
+ - [POST requests](#post-requests)
9
+ - [Other HTTP methods](#other-http-methods)
10
+ - [Following redirects](#following-redirects)
11
+ - [Custom headers](#custom-headers)
12
+ - [Inspecting responses](#inspecting-responses)
13
+ - [JSON responses](#json-responses)
14
+ - [Response attributes](#response-attributes)
15
+ - [Authentication](#authentication)
16
+ - [Sessions](#sessions)
17
+ - [RequestFactory](#requestfactory)
18
+ - [FAQs](#faqs)
19
+ - [What is the difference between Client and RequestFactory?](#what-is-the-difference-between-client-and-requestfactory)
20
+ - [How do I test file uploads?](#how-do-i-test-file-uploads)
21
+ - [How do I disable exception raising?](#how-do-i-disable-exception-raising)
22
+ - [Installation](#installation)
6
23
 
7
24
  ## Overview
8
25
 
9
- This module provides the [`Client`](client.py#Client) and [`RequestFactory`](client.py#RequestFactory) classes to facilitate testing requests and responses.
26
+ You can test your Plain views using the [`Client`](./client.py#Client) class. It simulates HTTP requests and returns responses, allowing you to verify status codes, content, and behavior without running a real server.
10
27
 
11
28
  ```python
12
29
  from plain.test import Client
13
30
 
14
31
 
15
- def test_client_example():
32
+ def test_homepage():
16
33
  client = Client()
17
-
18
- # Getting responses
19
34
  response = client.get("/")
20
35
  assert response.status_code == 200
36
+ ```
21
37
 
22
- # Modifying sessions
23
- client.session["example"] = "value"
24
- assert client.session["example"] == "value"
38
+ The client maintains cookies and session state across requests, so you can test multi-step flows like login and logout.
25
39
 
26
- # Logging in
27
- user = User.query.first()
28
- client.force_login(user)
29
- response = client.get("/protected/")
30
- assert response.status_code == 200
40
+ ## Making requests
41
+
42
+ ### GET requests
43
+
44
+ Pass query parameters using the `data` argument.
45
+
46
+ ```python
47
+ response = client.get("/search/", data={"q": "hello"})
48
+ ```
49
+
50
+ ### POST requests
51
+
52
+ Send form data by default.
53
+
54
+ ```python
55
+ response = client.post("/submit/", data={"name": "Alice", "email": "alice@example.com"})
56
+ ```
57
+
58
+ Send JSON by setting the content type.
59
+
60
+ ```python
61
+ response = client.post(
62
+ "/api/users/",
63
+ data={"name": "Alice"},
64
+ content_type="application/json",
65
+ )
66
+ ```
67
+
68
+ ### Other HTTP methods
69
+
70
+ The client supports all standard HTTP methods: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, and `trace`.
71
+
72
+ ```python
73
+ response = client.put("/api/users/1/", data={"name": "Bob"}, content_type="application/json")
74
+ response = client.patch("/api/users/1/", data={"name": "Bob"}, content_type="application/json")
75
+ response = client.delete("/api/users/1/")
76
+ ```
77
+
78
+ ### Following redirects
79
+
80
+ Set `follow=True` to automatically follow redirect responses.
81
+
82
+ ```python
83
+ response = client.get("/old-url/", follow=True)
84
+ assert response.status_code == 200 # Final destination
85
+ assert response.redirect_chain == [("/new-url/", 302)]
86
+ ```
87
+
88
+ ### Custom headers
89
+
90
+ Pass headers using the `headers` argument.
91
+
92
+ ```python
93
+ response = client.get("/api/", headers={"Authorization": "Bearer token123"})
94
+ ```
95
+
96
+ You can also set default headers when creating the client.
97
+
98
+ ```python
99
+ client = Client(headers={"Accept-Language": "en-US"})
100
+ ```
101
+
102
+ ## Inspecting responses
103
+
104
+ ### JSON responses
105
+
106
+ Parse JSON response content using the `json()` method.
107
+
108
+ ```python
109
+ response = client.get("/api/users/")
110
+ data = response.json()
111
+ assert data["users"][0]["name"] == "Alice"
112
+ ```
113
+
114
+ ### Response attributes
115
+
116
+ The [`ClientResponse`](./client.py#ClientResponse) wrapper provides access to:
117
+
118
+ - `status_code` - HTTP status code (200, 404, etc.)
119
+ - `content` - Response body as bytes
120
+ - `headers` - Response headers
121
+ - `cookies` - Cookies set by the response
122
+ - `wsgi_request` - The original request object
123
+ - `resolver_match` - URL resolver match information
124
+ - `redirect_chain` - List of redirects when using `follow=True`
125
+
126
+ ## Authentication
127
+
128
+ You can log in a user without going through the login flow using `force_login`. This requires `plain.auth` to be installed.
129
+
130
+ ```python
131
+ user = User.objects.get(username="alice")
132
+ client.force_login(user)
133
+ response = client.get("/dashboard/")
134
+ assert response.status_code == 200
135
+ ```
136
+
137
+ Log out using the `logout` method.
138
+
139
+ ```python
140
+ client.logout()
141
+ response = client.get("/dashboard/")
142
+ assert response.status_code == 302 # Redirected to login
143
+ ```
144
+
145
+ ## Sessions
146
+
147
+ Access session data using the `session` property. This requires `plain.sessions` to be installed.
148
+
149
+ ```python
150
+ client.session["cart_id"] = "abc123"
151
+ response = client.get("/cart/")
152
+ assert "abc123" in response.content.decode()
153
+ ```
154
+
155
+ ## RequestFactory
31
156
 
32
- # Logging out
33
- client.logout()
34
- response = client.get("/protected/")
35
- assert response.status_code == 302
157
+ Use [`RequestFactory`](./client.py#RequestFactory) to create request objects directly without going through the WSGI handler. This is useful for testing views in isolation.
36
158
 
37
- def test_request_factory_example():
38
- request = RequestFactory().get("/")
39
- assert request.method == "GET"
159
+ ```python
160
+ from plain.test import RequestFactory
161
+
162
+ rf = RequestFactory()
163
+ request = rf.get("/hello/")
164
+ response = my_view(request)
165
+ assert response.status_code == 200
166
+ ```
167
+
168
+ The factory supports the same HTTP methods as the client: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, and `trace`.
169
+
170
+ ## FAQs
171
+
172
+ #### What is the difference between Client and RequestFactory?
173
+
174
+ The `Client` executes requests through the full middleware stack and maintains state (cookies, sessions) between requests. Use it for integration tests.
175
+
176
+ The `RequestFactory` creates request objects without executing them. Use it for unit testing individual views in isolation.
177
+
178
+ #### How do I test file uploads?
179
+
180
+ Pass file-like objects in the `data` dictionary.
181
+
182
+ ```python
183
+ from io import BytesIO
184
+
185
+ file = BytesIO(b"file contents")
186
+ file.name = "test.txt"
187
+ response = client.post("/upload/", data={"file": file})
188
+ ```
189
+
190
+ #### How do I disable exception raising?
191
+
192
+ By default, the client raises exceptions that occur during request processing. Set `raise_request_exception=False` to capture them on the response instead.
193
+
194
+ ```python
195
+ client = Client(raise_request_exception=False)
196
+ response = client.get("/broken/")
197
+ assert response.status_code == 500
40
198
  ```
41
199
 
42
- More complete testing utilities are provided by the [`plain.pytest`](/plain-pytest/README.md) package. The [`plain.models`](/plain-models/README.md) package also provides pytest fixtures for database testing.
200
+ ## Installation
201
+
202
+ The `plain.test` module is included with the `plain` package. No additional installation is required.
203
+
204
+ For additional testing utilities like pytest fixtures and browser testing, see [`plain.pytest`](/plain-pytest/README.md).