plain 0.81.0__py3-none-any.whl → 0.83.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.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

plain/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
4
+
5
+ ### What's changed
6
+
7
+ - Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
8
+ - The `json_script` utility function now accepts an optional `nonce` parameter for CSP-compliant inline JSON scripts ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - Any `|json_script` usages need to make sure the second argument is a nonce, not a custom encoder (which is now third)
13
+
14
+ ## [0.82.0](https://github.com/dropseed/plain/releases/plain@0.82.0) (2025-10-29)
15
+
16
+ ### What's changed
17
+
18
+ - The `DEFAULT_RESPONSE_HEADERS` setting can now be a callable that accepts a request argument, enabling dynamic header generation per request ([cb92905834](https://github.com/dropseed/plain/commit/cb92905834))
19
+ - Added `request.csp_nonce` cached property for generating Content Security Policy nonces ([75071dcc70](https://github.com/dropseed/plain/commit/75071dcc70))
20
+ - Simplified the preflight command by moving `plain preflight check` back to `plain preflight` ([40c2c4560e](https://github.com/dropseed/plain/commit/40c2c4560e))
21
+
22
+ ### Upgrade instructions
23
+
24
+ - If you use `plain preflight check`, update to `plain preflight` (the `check` subcommand has been removed for simplicity)
25
+ - If you use `plain preflight check --deploy`, update to `plain preflight --deploy`
26
+
3
27
  ## [0.81.0](https://github.com/dropseed/plain/releases/plain@0.81.0) (2025-10-22)
4
28
 
5
29
  ### What's changed
plain/cli/preflight.py CHANGED
@@ -5,21 +5,13 @@ import click
5
5
 
6
6
  from plain import preflight
7
7
  from plain.packages import packages_registry
8
- from plain.preflight.registry import checks_registry
9
- from plain.runtime import settings
10
8
 
11
9
 
12
- @click.group("preflight")
13
- def preflight_cli() -> None:
14
- """Run or manage preflight checks."""
15
- pass
16
-
17
-
18
- @preflight_cli.command("check")
10
+ @click.command("preflight")
19
11
  @click.option(
20
12
  "--deploy",
21
13
  is_flag=True,
22
- help="Check deployment settings.",
14
+ help="Include deployment checks.",
23
15
  )
24
16
  @click.option(
25
17
  "--format",
@@ -32,14 +24,13 @@ def preflight_cli() -> None:
32
24
  is_flag=True,
33
25
  help="Hide progress output and warnings, only show errors.",
34
26
  )
35
- def check_command(deploy: bool, format: str, quiet: bool) -> None:
27
+ def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
36
28
  """
37
- Use the system check framework to validate entire Plain project.
29
+ Run preflight checks to validate your Plain project.
38
30
  Exit with error code if any errors are found. Warnings do not cause failure.
39
31
  """
40
32
  # Auto-discover and load preflight checks
41
33
  packages_registry.autodiscover_modules("preflight", include_app=True)
42
-
43
34
  if not quiet:
44
35
  click.secho("Running preflight checks...", dim=True, italic=True, err=True)
45
36
 
@@ -200,48 +191,3 @@ def check_command(deploy: bool, format: str, quiet: bool) -> None:
200
191
  # Exit with error if there are any errors (not warnings)
201
192
  if has_errors:
202
193
  sys.exit(1)
203
-
204
-
205
- @preflight_cli.command("list")
206
- def list_checks() -> None:
207
- """List all available preflight checks."""
208
- packages_registry.autodiscover_modules("preflight", include_app=True)
209
-
210
- regular = []
211
- deployment = []
212
- silenced_checks = settings.PREFLIGHT_SILENCED_CHECKS
213
-
214
- for name, (check_class, deploy) in sorted(checks_registry.checks.items()):
215
- # Use class docstring as description
216
- description = check_class.__doc__ or "No description"
217
- # Get first line of docstring
218
- description = description.strip().split("\n")[0]
219
-
220
- is_silenced = name in silenced_checks
221
- if deploy:
222
- deployment.append((name, description, is_silenced))
223
- else:
224
- regular.append((name, description, is_silenced))
225
-
226
- if regular:
227
- click.echo("Regular checks:")
228
- for name, description, is_silenced in regular:
229
- silenced_text = (
230
- click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
231
- )
232
- click.echo(
233
- f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
234
- )
235
-
236
- if deployment:
237
- click.echo("\nDeployment checks:")
238
- for name, description, is_silenced in deployment:
239
- silenced_text = (
240
- click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
241
- )
242
- click.echo(
243
- f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
244
- )
245
-
246
- if not regular and not deployment:
247
- click.echo("No preflight checks found.")
plain/http/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  **HTTP request and response handling.**
4
4
 
5
5
  - [Overview](#overview)
6
+ - [Content Security Policy (CSP)](#content-security-policy-csp)
6
7
 
7
8
  ## Overview
8
9
 
@@ -28,3 +29,61 @@ class ExampleView(View):
28
29
 
29
30
  return response
30
31
  ```
32
+
33
+ ## Content Security Policy (CSP)
34
+
35
+ Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
36
+
37
+ Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
38
+
39
+ ### Configuring CSP Headers
40
+
41
+ Set `DEFAULT_RESPONSE_HEADERS` as a callable function to generate dynamic CSP headers with nonces:
42
+
43
+ ```python
44
+ # app/settings.py
45
+ def DEFAULT_RESPONSE_HEADERS(request):
46
+ """
47
+ Dynamic response headers with CSP nonces.
48
+ """
49
+ nonce = request.csp_nonce
50
+ return {
51
+ "Content-Security-Policy": (
52
+ f"default-src 'self'; "
53
+ f"script-src 'self' 'nonce-{nonce}'; "
54
+ f"style-src 'self' 'nonce-{nonce}'; "
55
+ f"img-src 'self' data:; "
56
+ f"font-src 'self'; "
57
+ f"connect-src 'self'; "
58
+ f"frame-ancestors 'self'; "
59
+ f"base-uri 'self'; "
60
+ f"form-action 'self'"
61
+ ),
62
+ }
63
+ ```
64
+
65
+ Use tools like [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com/) to analyze your CSP policy and identify potential security issues or misconfigurations.
66
+
67
+ ### Using Nonces in Templates
68
+
69
+ Add the nonce attribute to inline scripts and styles in your templates:
70
+
71
+ ```html
72
+ <!-- Inline script with nonce -->
73
+ <script nonce="{{ request.csp_nonce }}">
74
+ console.log("This script is allowed by CSP");
75
+ </script>
76
+
77
+ <!-- Inline style with nonce -->
78
+ <style nonce="{{ request.csp_nonce }}">
79
+ .example { color: red; }
80
+ </style>
81
+ ```
82
+
83
+ External scripts and stylesheets loaded from `'self'` don't need nonces:
84
+
85
+ ```html
86
+ <!-- External scripts/styles work with 'self' directive -->
87
+ <script src="/assets/app.js"></script>
88
+ <link rel="stylesheet" href="/assets/app.css">
89
+ ```
plain/http/request.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import codecs
4
4
  import copy
5
5
  import json
6
+ import secrets
6
7
  import uuid
7
8
  from collections.abc import Iterator
8
9
  from functools import cached_property
@@ -101,6 +102,16 @@ class Request:
101
102
  def headers(self) -> RequestHeaders:
102
103
  return RequestHeaders(self.meta)
103
104
 
105
+ @cached_property
106
+ def csp_nonce(self) -> str:
107
+ """Generate a cryptographically secure nonce for Content Security Policy.
108
+
109
+ The nonce is generated once per request and cached. It can be used in
110
+ CSP headers and templates to allow specific inline scripts/styles while
111
+ blocking others.
112
+ """
113
+ return secrets.token_urlsafe(16)
114
+
104
115
  @cached_property
105
116
  def accepted_types(self) -> list[MediaType]:
106
117
  """Return accepted media types sorted by quality value (highest first).
@@ -13,8 +13,15 @@ class DefaultHeadersMiddleware(HttpMiddleware):
13
13
  def process_request(self, request: Request) -> Response:
14
14
  response = self.get_response(request)
15
15
 
16
- for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
17
- # Since we don't have a good way to *remote* default response headers,
16
+ # Support callable DEFAULT_RESPONSE_HEADERS for dynamic header generation
17
+ # (e.g., CSP nonces that change per request)
18
+ if callable(settings.DEFAULT_RESPONSE_HEADERS):
19
+ default_headers = settings.DEFAULT_RESPONSE_HEADERS(request)
20
+ else:
21
+ default_headers = settings.DEFAULT_RESPONSE_HEADERS
22
+
23
+ for header, value in default_headers.items():
24
+ # Since we don't have a good way to *remove* default response headers,
18
25
  # use allow users to set them to an empty string to indicate they should be removed.
19
26
  if header in response.headers and response.headers[header] == "":
20
27
  del response.headers[header]
plain/preflight/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  Preflight checks help identify issues with your settings or environment before running your application.
14
14
 
15
15
  ```bash
16
- plain preflight check
16
+ plain preflight
17
17
  ```
18
18
 
19
19
  ## Development
@@ -22,31 +22,53 @@ If you use [`plain.dev`](/plain-dev/README.md) for local development, the Plain
22
22
 
23
23
  ## Deployment
24
24
 
25
- The `plain preflight check` command should often be part of your deployment process. Make sure to add the `--deploy` flag to the command to run checks that are only relevant in a production environment.
25
+ The `plain preflight` command should often be part of your deployment process. Make sure to add the `--deploy` flag to the command to run checks that are only relevant in a production environment.
26
26
 
27
27
  ```bash
28
- plain preflight check --deploy
28
+ plain preflight --deploy
29
29
  ```
30
30
 
31
31
  ## Custom preflight checks
32
32
 
33
- Use the `@register_check` decorator to add your own preflight check to the system. Just make sure that particular Python module is somehow imported so the check registration runs.
33
+ Use the `@register_check` decorator to add your own preflight check to the system. Create a class that inherits from `PreflightCheck` and implements a `run()` method that returns a list of `PreflightResult` objects.
34
34
 
35
35
  ```python
36
- from plain.preflight import register_check, Error
37
-
38
-
39
- @register_check
40
- def custom_check(package_configs, **kwargs):
41
- return Error("This is a custom error message.", id="custom.C001")
36
+ from plain.preflight import PreflightCheck, PreflightResult, register_check
37
+
38
+
39
+ @register_check("custom.example")
40
+ class CustomCheck(PreflightCheck):
41
+ """Description of what this check validates."""
42
+
43
+ def run(self) -> list[PreflightResult]:
44
+ # Your check logic here
45
+ if some_condition:
46
+ return [
47
+ PreflightResult(
48
+ fix="This is a custom error message.",
49
+ id="custom.example_failed",
50
+ )
51
+ ]
52
+ return []
42
53
  ```
43
54
 
44
- For deployment-specific checks, add the `deploy` argument to the decorator.
55
+ For deployment-specific checks, add `deploy=True` to the decorator.
45
56
 
46
57
  ```python
47
- @register_check(deploy=True)
48
- def custom_deploy_check(package_configs, **kwargs):
49
- return Error("This is a custom error message for deployment.", id="custom.D001")
58
+ @register_check("custom.deploy_example", deploy=True)
59
+ class CustomDeployCheck(PreflightCheck):
60
+ """Description of what this deployment check validates."""
61
+
62
+ def run(self) -> list[PreflightResult]:
63
+ # Your deployment check logic here
64
+ if some_deploy_condition:
65
+ return [
66
+ PreflightResult(
67
+ fix="This is a custom error message for deployment.",
68
+ id="custom.deploy_example_failed",
69
+ )
70
+ ]
71
+ return []
50
72
  ```
51
73
 
52
74
  ## Silencing preflight checks
plain/utils/html.py CHANGED
@@ -34,25 +34,30 @@ _json_script_escapes = {
34
34
  def json_script(
35
35
  value: Any,
36
36
  element_id: str | None = None,
37
+ nonce: str = "",
37
38
  encoder: type[json.JSONEncoder] | None = None,
38
39
  ) -> SafeString:
39
40
  """
40
41
  Escape all the HTML/XML special characters with their unicode escapes, so
41
42
  value is safe to be output anywhere except for inside a tag attribute. Wrap
42
43
  the escaped JSON in a script tag.
44
+
45
+ Args:
46
+ value: The data to encode as JSON
47
+ element_id: Optional ID attribute for the script tag
48
+ nonce: Optional CSP nonce for inline script tags
49
+ encoder: Optional custom JSON encoder class
43
50
  """
44
51
  from plain.json import PlainJSONEncoder
45
52
 
46
53
  json_str = json.dumps(value, cls=encoder or PlainJSONEncoder).translate(
47
54
  _json_script_escapes
48
55
  )
49
- if element_id:
50
- template = '<script id="{}" type="application/json">{}</script>'
51
- args = (element_id, mark_safe(json_str))
52
- else:
53
- template = '<script type="application/json">{}</script>'
54
- args = (mark_safe(json_str),)
55
- return format_html(template, *args)
56
+ id_attr = f' id="{element_id}"' if element_id else ""
57
+ nonce_attr = f' nonce="{nonce}"' if nonce else ""
58
+ return mark_safe(
59
+ f'<script{id_attr}{nonce_attr} type="application/json">{json_str}</script>'
60
+ )
56
61
 
57
62
 
58
63
  def conditional_escape(text: Any) -> SafeString | str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.81.0
3
+ Version: 0.83.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,5 +1,5 @@
1
1
  plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
2
- plain/CHANGELOG.md,sha256=X4IioATkhRixLtoG5oftPX7QJ5NmVUyywMqmUIGBTck,32215
2
+ plain/CHANGELOG.md,sha256=xY-6RVqtfACU0z3588U3ALK_B3tuaNuBbp1d2J8qFf8,33715
3
3
  plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
4
4
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
5
5
  plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
@@ -30,7 +30,7 @@ plain/cli/docs.py,sha256=PU3v7Z7qgYFG-bClpuDg4JeWwC8uvLYX3ovkQDMseVs,1146
30
30
  plain/cli/formatting.py,sha256=e1doTFalAM11bD_Cvqeu6sTap81JrQcB-4kMjZzAHmY,2737
31
31
  plain/cli/install.py,sha256=mffSYBmSJSj44OPBfu53nBQoyoz4jk69DvppubIB0mU,2791
32
32
  plain/cli/output.py,sha256=uZTHZR-Axeoi2r6fgcDCpDA7iQSRrktBtTf1yBT5djI,1426
33
- plain/cli/preflight.py,sha256=5UXOowjiCMqsGIrpHg60f6Ptjk40rMiDSowN8wy5jSY,8541
33
+ plain/cli/preflight.py,sha256=zpFTkk4yET3mosFBxHY5B-vSlLu1LXvAZ3hrcUZG-oo,6721
34
34
  plain/cli/print.py,sha256=7kv9ddXpwOHRSWp6FFLfX4wbmhV7neoOBlE0VcXWccw,238
35
35
  plain/cli/registry.py,sha256=Z52nVE2bC2h_B_SENnXctn3mx3UWB0qYg969DVP7XX8,1106
36
36
  plain/cli/runtime.py,sha256=YbGYfwkH0VxfuIMbOCwM9wSWiQKusPn_gVeGod8OFaE,743
@@ -56,12 +56,12 @@ plain/forms/boundfield.py,sha256=PEquPRn1BoVd4ZkIin8tjkBzRDMv-41wO8qHV0S1shg,196
56
56
  plain/forms/exceptions.py,sha256=MuMUF-33Qsc_HWk5zI3rcWzdvXzQbEOKAZvJKkbrL58,320
57
57
  plain/forms/fields.py,sha256=GQSTI6-eaHivIXj3TQSw2LpyWMbN0NZ-SHjUO_oxqeA,37132
58
58
  plain/forms/forms.py,sha256=GnJWzjIXOPByfrdiqjjo4xdKdkBw1gBD6Hq4mg73gHQ,11259
59
- plain/http/README.md,sha256=32uuWbarAcG_qmP-Fltk-_26HFKfY3dBdlrO2FDb7D0,756
59
+ plain/http/README.md,sha256=8FLsZ12Gx6lLa0mFMDcqWYetM6ysCvfHMZLLh4dAAVI,2611
60
60
  plain/http/__init__.py,sha256=gUTIGh-GbSIlh3SP-Db-XAOX4P_j2hh2445w8Cm-zKQ,1032
61
61
  plain/http/cookie.py,sha256=x13G3LIr0jxnPK1NQRptmi0DrAq9PsivQnQTm4LKaW0,2191
62
62
  plain/http/middleware.py,sha256=TPs585IIFjgp-5uUAJtIoigH6uwTS3FJqwFSsQdayd4,960
63
63
  plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs,28350
64
- plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
64
+ plain/http/request.py,sha256=JexbxZ7EXbvi-J9kntAdIwR9p3vk2Cekb6UoFJXfK74,26850
65
65
  plain/http/response.py,sha256=9AlV1PfBsimNFW5LV-o9xcRvp1uXEFeXMnq1vAYPYh0,24971
66
66
  plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
67
67
  plain/internal/reloader.py,sha256=n7B-F-WeUXp37pAnvzKX9tcEbUxHSlYqa4gItyA_zko,2662
@@ -78,7 +78,7 @@ plain/internal/handlers/base.py,sha256=ZlpOYqd45X0wPYlmxHwXR5kyCkWBB6ZJeSGZka5VA
78
78
  plain/internal/handlers/exception.py,sha256=P7oTqVtKoIHBOxCml1BSgNBV_rQWyCHlO5hS87NVjXo,4872
79
79
  plain/internal/handlers/wsgi.py,sha256=d2Hcs4fzTSir6OvtpVromTw0RmmFAqTL4_EaBjoHtNU,8919
80
80
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
- plain/internal/middleware/headers.py,sha256=-lgba1Em1FiqFbaeI7bwCkd4yI-UuOnEf3vh1eKK87Q,1100
81
+ plain/internal/middleware/headers.py,sha256=z3rmjIGGYV5JcAfAUm3tcdG34OFoEfYukJrwZsq7JIo,1425
82
82
  plain/internal/middleware/hosts.py,sha256=UXMts7cKA9ocOK-J8DAcZPbKYQtfVyLSLH2QQX3mKjM,5895
83
83
  plain/internal/middleware/https.py,sha256=8n990O0Ej5cRrBlfAnLW2nZ2mx0wn0002C4HuRhVpsM,1117
84
84
  plain/internal/middleware/slash.py,sha256=V_rTb9v8uMUR_N2TLETBwlJFItoL4WD9JnRyFaFs878,3073
@@ -92,7 +92,7 @@ plain/packages/README.md,sha256=iNqMtwFDVNf2TqKUzLKQW5Y4_GsssmdB4cVerzu27Ro,2674
92
92
  plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
93
93
  plain/packages/config.py,sha256=dxs_i-z6noQF_6j3lq11mhnQ1Bj10u2CXTJY0JgeQgc,3166
94
94
  plain/packages/registry.py,sha256=bmqY1rQau4MRpbf6DXkUVlqF4XJ9Mz2awSi5E7tCGd4,9032
95
- plain/preflight/README.md,sha256=vR43F_ls81hRSo7J2NNZ4VOMoRaJ1bS5JwA6l4ez36g,1782
95
+ plain/preflight/README.md,sha256=tH3KctMH_Zrz4ohZXlhQAGuZmPw1mMa4jr-iE0SHvC4,2470
96
96
  plain/preflight/__init__.py,sha256=-uBIVLD1DlJUVypQsEcrOtaNAhECbOpKhyoz0c_WMhA,416
97
97
  plain/preflight/checks.py,sha256=kJcr-Hq5bsjKw1BUYR6r6nFg3Ecgrd1DS5SudUr4rSU,289
98
98
  plain/preflight/files.py,sha256=OjD76e-l_cDXJGHMk21LsoPp6V_HxD5v0zyvOKkEDu0,840
@@ -167,7 +167,7 @@ plain/utils/duration.py,sha256=QBFh-iLvRXuw0N0UUdFqabeJvZVqkgFME_c2gRSLwhI,1340
167
167
  plain/utils/encoding.py,sha256=40siECldXUFyZ_IkyQCeOkyQuXb7i4Rpys3nFWFHnuc,4482
168
168
  plain/utils/functional.py,sha256=HqRXyM6R3Sl9IbhGl6t-DdFwtfr05IYmq7rgFCWV_2U,14650
169
169
  plain/utils/hashable.py,sha256=1Kh_SFxsaR2d3xQMNEtHb4eKSHugtNt66iZ3ZvKjt50,811
170
- plain/utils/html.py,sha256=x9kruRAzs9mIJ3XlSmVcxIXOy-lSupZ4J3J0KlWr1Rc,4028
170
+ plain/utils/html.py,sha256=ctgzowSi_NkytPOw3FvyqEIXgOE-52trkTFPjiGvGVg,4202
171
171
  plain/utils/http.py,sha256=P7dkgWpC28Bm-_VNuj57VN8j8AdbaDu1CKxoggwBpKo,5847
172
172
  plain/utils/inspect.py,sha256=wbWDAiVa4MW4lwJZz-mvU8Db-I0Nq5DhHh9ew5w81EE,1480
173
173
  plain/utils/ipv6.py,sha256=TLOQVN0aqKGM4eS_HwarTgO-bGIql-k5AipDPgtQdCA,1352
@@ -188,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
188
188
  plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
189
189
  plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
190
190
  plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
191
- plain-0.81.0.dist-info/METADATA,sha256=dkk1DwWeDQbFb9gvY4x_JKN2p5bzCFkx2nKm7xqf9po,4516
192
- plain-0.81.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
- plain-0.81.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
- plain-0.81.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
- plain-0.81.0.dist-info/RECORD,,
191
+ plain-0.83.0.dist-info/METADATA,sha256=hiux2R0QdqLHhIroLLNQdEyMh4ZnKnfzJod0bGsGNw0,4516
192
+ plain-0.83.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
+ plain-0.83.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
+ plain-0.83.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
+ plain-0.83.0.dist-info/RECORD,,
File without changes