plain 0.72.0__py3-none-any.whl → 0.72.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.
plain/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.72.2](https://github.com/dropseed/plain/releases/plain@0.72.2) (2025-10-06)
4
+
5
+ ### What's changed
6
+
7
+ - Improved type annotations for test client responses with new `ClientResponse` wrapper class ([369353f9d6](https://github.com/dropseed/plain/commit/369353f9d6))
8
+ - Enhanced internal type checking for WSGI handler and request/response types ([50463b00c3](https://github.com/dropseed/plain/commit/50463b00c3))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
14
+ ## [0.72.1](https://github.com/dropseed/plain/releases/plain@0.72.1) (2025-10-02)
15
+
16
+ ### What's changed
17
+
18
+ - Fixed documentation examples to use the correct view attribute names (`self.user` instead of `self.request.user`) ([f6278d9](https://github.com/dropseed/plain/commit/f6278d9bb4))
19
+
20
+ ### Upgrade instructions
21
+
22
+ - No changes required
23
+
3
24
  ## [0.72.0](https://github.com/dropseed/plain/releases/plain@0.72.0) (2025-10-02)
4
25
 
5
26
  ### What's changed
@@ -18,7 +18,7 @@ from .exception import convert_exception_to_response
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Callable
20
20
 
21
- from plain.http import Request, Response
21
+ from plain.http import Request, Response, ResponseBase
22
22
  from plain.urls import ResolverMatch
23
23
 
24
24
  logger = logging.getLogger("plain.request")
@@ -72,7 +72,7 @@ class BaseHandler:
72
72
  # as a flag for initialization being complete.
73
73
  self._middleware_chain = handler
74
74
 
75
- def get_response(self, request: Request) -> Response:
75
+ def get_response(self, request: Request) -> ResponseBase:
76
76
  """Return a Response object for the given Request."""
77
77
 
78
78
  span_attributes = {
@@ -124,7 +124,7 @@ class BaseHandler:
124
124
  )
125
125
  return response
126
126
 
127
- def _get_response(self, request: Request) -> Response:
127
+ def _get_response(self, request: Request) -> ResponseBase:
128
128
  """
129
129
  Resolve and call the view, then apply view, exception, and
130
130
  template_response middleware. This method is everything that happens
@@ -16,6 +16,8 @@ if TYPE_CHECKING:
16
16
  from collections.abc import Callable, Iterable
17
17
  from typing import Any
18
18
 
19
+ from plain.http import ResponseBase
20
+
19
21
  _slashes_re = _lazy_re_compile(rb"/+")
20
22
 
21
23
 
@@ -141,7 +143,7 @@ class WSGIHandler(base.BaseHandler):
141
143
  self,
142
144
  environ: dict[str, Any],
143
145
  start_response: Callable[[str, list[tuple[str, str]]], Any],
144
- ) -> Iterable[bytes]:
146
+ ) -> ResponseBase | Iterable[bytes]:
145
147
  signals.request_started.send(sender=self.__class__, environ=environ)
146
148
  request = WSGIRequest(environ)
147
149
  response = self.get_response(request)
@@ -6,15 +6,11 @@ from collections import Counter
6
6
  from collections.abc import Iterable
7
7
  from importlib import import_module
8
8
  from importlib.util import find_spec
9
- from typing import TYPE_CHECKING
10
9
 
11
10
  from plain.exceptions import ImproperlyConfigured, PackageRegistryNotReady
12
11
 
13
12
  from .config import PackageConfig
14
13
 
15
- if TYPE_CHECKING:
16
- pass
17
-
18
14
  CONFIG_MODULE_NAME = "config"
19
15
 
20
16
 
plain/test/client.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import sys
5
- from functools import partial
6
5
  from http import HTTPStatus
7
6
  from http.cookies import SimpleCookie
8
7
  from io import BytesIO, IOBase
@@ -26,10 +25,12 @@ from .encoding import encode_multipart
26
25
  from .exceptions import RedirectCycleError
27
26
 
28
27
  if TYPE_CHECKING:
29
- from plain.http import Response
28
+ from plain.http import Response, ResponseBase
29
+ from plain.urls import ResolverMatch
30
30
 
31
31
  __all__ = (
32
32
  "Client",
33
+ "ClientResponse",
33
34
  "RequestFactory",
34
35
  )
35
36
 
@@ -41,6 +42,79 @@ _CONTENT_TYPE_RE = _lazy_re_compile(r".*; charset=([\w-]+);?")
41
42
  _JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
42
43
 
43
44
 
45
+ class ClientResponse:
46
+ """
47
+ Response wrapper returned by test Client with test-specific attributes.
48
+
49
+ Wraps any ResponseBase subclass and adds attributes useful for testing,
50
+ while delegating all other attribute access to the wrapped response.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ response: ResponseBase,
56
+ client: Client,
57
+ request: dict[str, Any],
58
+ exc_info: tuple[Any, Any, Any] | None,
59
+ ):
60
+ # Store wrapped response in __dict__ directly to avoid __setattr__ recursion
61
+ object.__setattr__(self, "_response", response)
62
+ object.__setattr__(self, "_json_cache", None)
63
+ # Test-specific attributes
64
+ self.client = client
65
+ self.request = request
66
+ self.wsgi_request: WSGIRequest
67
+ self.redirect_chain: list[tuple[str, int]]
68
+ self.resolver_match: SimpleLazyObject | ResolverMatch
69
+ self.exc_info = exc_info
70
+ # Optional: set by plain.auth if available
71
+ # self.user: Model
72
+
73
+ def json(self, **extra: Any) -> Any:
74
+ """Parse response content as JSON."""
75
+ _json_cache = object.__getattribute__(self, "_json_cache")
76
+ if _json_cache is None:
77
+ response = object.__getattribute__(self, "_response")
78
+ content_type = response.headers.get("Content-Type", "")
79
+ if not _JSON_CONTENT_TYPE_RE.match(content_type):
80
+ raise ValueError(
81
+ f'Content-Type header is "{content_type}", not "application/json"'
82
+ )
83
+ _json_cache = json.loads(
84
+ response.content.decode(response.charset),
85
+ **extra,
86
+ )
87
+ object.__setattr__(self, "_json_cache", _json_cache)
88
+ return _json_cache
89
+
90
+ @property
91
+ def url(self) -> str:
92
+ """
93
+ Return redirect URL if this is a redirect response.
94
+
95
+ This property exists on ResponseRedirect and is added for redirects.
96
+ """
97
+ response = object.__getattribute__(self, "_response")
98
+ if hasattr(response, "url"):
99
+ return response.url # type: ignore[attr-defined,return-value]
100
+ # For non-redirect responses, try to get Location header
101
+ if "Location" in response.headers:
102
+ return response.headers["Location"]
103
+ raise AttributeError(f"{response.__class__.__name__} has no attribute 'url'")
104
+
105
+ def __getattr__(self, name: str) -> Any:
106
+ """Delegate attribute access to the wrapped response."""
107
+ return getattr(object.__getattribute__(self, "_response"), name)
108
+
109
+ def __setattr__(self, name: str, value: Any) -> None:
110
+ """Set attributes on the wrapper itself."""
111
+ object.__setattr__(self, name, value)
112
+
113
+ def __repr__(self) -> str:
114
+ """Return repr of wrapped response."""
115
+ return repr(object.__getattribute__(self, "_response"))
116
+
117
+
44
118
  @internalcode
45
119
  class FakePayload(IOBase):
46
120
  """
@@ -470,7 +544,7 @@ class Client:
470
544
  """Set the cookies on the request factory."""
471
545
  self._request_factory.cookies = value
472
546
 
473
- def request(self, **request: Any) -> Response:
547
+ def request(self, **request: Any) -> ClientResponse:
474
548
  """
475
549
  Make a generic request. Compose the environment dictionary and pass
476
550
  to the handler, return the result of the handler. Assume defaults for
@@ -487,31 +561,36 @@ class Client:
487
561
  finally:
488
562
  # signals.template_rendered.disconnect(dispatch_uid=signal_uid)
489
563
  got_request_exception.disconnect(dispatch_uid=exception_uid)
490
- # Check for signaled exceptions.
491
- self.check_exception(response)
492
- # Save the client and request that stimulated the response.
493
- response.client = self
494
- response.request = request
495
- response.json = partial(self._parse_json, response)
564
+
565
+ # Wrap the response in ClientResponse for test-specific attributes
566
+ client_response = ClientResponse(
567
+ response=response,
568
+ client=self,
569
+ request=request,
570
+ exc_info=self.exc_info,
571
+ )
572
+
573
+ # Check for signaled exceptions and potentially re-raise
574
+ self.check_exception()
496
575
 
497
576
  # If the request had a user, make it available on the response.
498
577
  try:
499
578
  from plain.auth.requests import get_request_user
500
579
 
501
- response.user = get_request_user(response.wsgi_request)
580
+ client_response.user = get_request_user(client_response.wsgi_request)
502
581
  except ImportError:
503
582
  pass
504
583
 
505
584
  # Attach the ResolverMatch instance to the response.
506
585
  resolver = get_resolver()
507
- response.resolver_match = SimpleLazyObject(
586
+ client_response.resolver_match = SimpleLazyObject(
508
587
  lambda: resolver.resolve(request["PATH_INFO"]),
509
588
  )
510
589
 
511
590
  # Update persistent cookie data.
512
- if response.cookies:
513
- self.cookies.update(response.cookies)
514
- return response
591
+ if client_response.cookies:
592
+ self.cookies.update(client_response.cookies)
593
+ return client_response
515
594
 
516
595
  def get(
517
596
  self,
@@ -522,7 +601,7 @@ class Client:
522
601
  *,
523
602
  headers: dict[str, str] | None = None,
524
603
  **extra: Any,
525
- ) -> Response:
604
+ ) -> ClientResponse:
526
605
  """Request a response from the server using GET."""
527
606
  self.extra = extra
528
607
  self.headers = headers
@@ -548,7 +627,7 @@ class Client:
548
627
  *,
549
628
  headers: dict[str, str] | None = None,
550
629
  **extra: Any,
551
- ) -> Response:
630
+ ) -> ClientResponse:
552
631
  """Request a response from the server using POST."""
553
632
  self.extra = extra
554
633
  self.headers = headers
@@ -578,7 +657,7 @@ class Client:
578
657
  *,
579
658
  headers: dict[str, str] | None = None,
580
659
  **extra: Any,
581
- ) -> Response:
660
+ ) -> ClientResponse:
582
661
  """Request a response from the server using HEAD."""
583
662
  self.extra = extra
584
663
  self.headers = headers
@@ -604,7 +683,7 @@ class Client:
604
683
  *,
605
684
  headers: dict[str, str] | None = None,
606
685
  **extra: Any,
607
- ) -> Response:
686
+ ) -> ClientResponse:
608
687
  """Request a response from the server using OPTIONS."""
609
688
  self.extra = extra
610
689
  self.headers = headers
@@ -635,7 +714,7 @@ class Client:
635
714
  *,
636
715
  headers: dict[str, str] | None = None,
637
716
  **extra: Any,
638
- ) -> Response:
717
+ ) -> ClientResponse:
639
718
  """Send a resource to the server using PUT."""
640
719
  self.extra = extra
641
720
  self.headers = headers
@@ -666,7 +745,7 @@ class Client:
666
745
  *,
667
746
  headers: dict[str, str] | None = None,
668
747
  **extra: Any,
669
- ) -> Response:
748
+ ) -> ClientResponse:
670
749
  """Send a resource to the server using PATCH."""
671
750
  self.extra = extra
672
751
  self.headers = headers
@@ -697,7 +776,7 @@ class Client:
697
776
  *,
698
777
  headers: dict[str, str] | None = None,
699
778
  **extra: Any,
700
- ) -> Response:
779
+ ) -> ClientResponse:
701
780
  """Send a DELETE request to the server."""
702
781
  self.extra = extra
703
782
  self.headers = headers
@@ -727,7 +806,7 @@ class Client:
727
806
  *,
728
807
  headers: dict[str, str] | None = None,
729
808
  **extra: Any,
730
- ) -> Response:
809
+ ) -> ClientResponse:
731
810
  """Send a TRACE request to the server."""
732
811
  self.extra = extra
733
812
  self.headers = headers
@@ -745,16 +824,16 @@ class Client:
745
824
 
746
825
  def _handle_redirects(
747
826
  self,
748
- response: Response,
827
+ response: ClientResponse,
749
828
  data: Any = "",
750
829
  content_type: str = "",
751
830
  headers: dict[str, str] | None = None,
752
831
  **extra: Any,
753
- ) -> Response:
832
+ ) -> ClientResponse:
754
833
  """
755
834
  Follow any redirects by requesting responses from the server using GET.
756
835
  """
757
- response.redirect_chain = [] # type: ignore[attr-defined]
836
+ response.redirect_chain = []
758
837
  redirect_status_codes = (
759
838
  HTTPStatus.MOVED_PERMANENTLY,
760
839
  HTTPStatus.FOUND,
@@ -763,8 +842,8 @@ class Client:
763
842
  HTTPStatus.PERMANENT_REDIRECT,
764
843
  )
765
844
  while response.status_code in redirect_status_codes:
766
- response_url = response.url # type: ignore[attr-defined]
767
- redirect_chain = response.redirect_chain # type: ignore[attr-defined]
845
+ response_url = response.url
846
+ redirect_chain = response.redirect_chain
768
847
  redirect_chain.append((response_url, response.status_code))
769
848
 
770
849
  url = urlsplit(response_url)
@@ -781,7 +860,7 @@ class Client:
781
860
  path = "/"
782
861
  # Prepend the request path to handle relative path redirects
783
862
  if not path.startswith("/"):
784
- path = urljoin(response.request["PATH_INFO"], path) # type: ignore[attr-defined]
863
+ path = urljoin(response.request["PATH_INFO"], path)
785
864
 
786
865
  if response.status_code in (
787
866
  HTTPStatus.TEMPORARY_REDIRECT,
@@ -789,7 +868,7 @@ class Client:
789
868
  ):
790
869
  # Preserve request method and query string (if needed)
791
870
  # post-redirect for 307/308 responses.
792
- request_method_name = response.request["REQUEST_METHOD"].lower() # type: ignore[attr-defined]
871
+ request_method_name = response.request["REQUEST_METHOD"].lower()
793
872
  if request_method_name not in ("get", "head"):
794
873
  extra["QUERY_STRING"] = url.query
795
874
  request_method = getattr(self, request_method_name)
@@ -806,7 +885,7 @@ class Client:
806
885
  headers=headers,
807
886
  **extra,
808
887
  )
809
- response.redirect_chain = redirect_chain # type: ignore[attr-defined]
888
+ response.redirect_chain = redirect_chain
810
889
 
811
890
  if redirect_chain[-1] in redirect_chain[:-1]:
812
891
  # Check that we're not redirecting to somewhere we've already
@@ -826,13 +905,8 @@ class Client:
826
905
  """Store exceptions when they are generated by a view."""
827
906
  self.exc_info = sys.exc_info()
828
907
 
829
- def check_exception(self, response: Response) -> None:
830
- """
831
- Look for a signaled exception, clear the current context exception
832
- data, re-raise the signaled exception, and clear the signaled exception
833
- from the local cache.
834
- """
835
- response.exc_info = self.exc_info # type: ignore[attr-defined]
908
+ def check_exception(self) -> None:
909
+ """Check for signaled exceptions and potentially re-raise."""
836
910
  if self.exc_info:
837
911
  _, exc_value, _ = self.exc_info
838
912
  self.exc_info = None
@@ -856,17 +930,3 @@ class Client:
856
930
  from plain.auth.test import logout_client
857
931
 
858
932
  logout_client(self)
859
-
860
- def _parse_json(self, response: Response, **extra: Any) -> Any:
861
- if not hasattr(response, "_json"):
862
- if not _JSON_CONTENT_TYPE_RE.match(response.headers.get("Content-Type")):
863
- raise ValueError(
864
- 'Content-Type header is "{}", not "application/json"'.format(
865
- response.headers.get("Content-Type")
866
- )
867
- )
868
- response._json = json.loads( # type: ignore[attr-defined]
869
- response.content.decode(response.charset),
870
- **extra, # type: ignore[arg-type]
871
- )
872
- return response._json # type: ignore[attr-defined]
plain/views/README.md CHANGED
@@ -181,7 +181,7 @@ class ExampleDetailView(DetailView):
181
181
  def get_object(self):
182
182
  return MyObjectClass.query.get(
183
183
  id=self.url_kwargs["id"],
184
- user=self.request.user, # Limit access
184
+ user=self.user, # Limit access
185
185
  )
186
186
 
187
187
 
@@ -199,7 +199,7 @@ class ExampleUpdateView(UpdateView):
199
199
  def get_object(self):
200
200
  return MyObjectClass.query.get(
201
201
  id=self.url_kwargs["id"],
202
- user=self.request.user, # Limit access
202
+ user=self.user, # Limit access
203
203
  )
204
204
 
205
205
 
@@ -213,7 +213,7 @@ class ExampleDeleteView(DeleteView):
213
213
  def get_object(self):
214
214
  return MyObjectClass.query.get(
215
215
  id=self.url_kwargs["id"],
216
- user=self.request.user, # Limit access
216
+ user=self.user, # Limit access
217
217
  )
218
218
 
219
219
 
@@ -222,7 +222,7 @@ class ExampleListView(ListView):
222
222
 
223
223
  def get_objects(self):
224
224
  return MyObjectClass.query.filter(
225
- user=self.request.user, # Limit access
225
+ user=self.user, # Limit access
226
226
  )
227
227
  ```
228
228
 
@@ -241,7 +241,7 @@ from plain.http import Response
241
241
 
242
242
  class ExampleView(DetailView):
243
243
  def get_object(self):
244
- if self.request.user.exceeds_rate_limit:
244
+ if self.user and self.user.exceeds_rate_limit:
245
245
  raise ResponseException(
246
246
  Response("Rate limit exceeded", status_code=429)
247
247
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.72.0
3
+ Version: 0.72.2
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=BtIi33MSvdQGi1d2UVzC0WSBA1fxMVAGHXaDkTSYLiY,22042
2
+ plain/CHANGELOG.md,sha256=m-g1Jycq3u9KeMyMUD4pSjJhubEoefzEaPvhppCaCcM,22837
3
3
  plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
4
4
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
5
5
  plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
@@ -70,9 +70,9 @@ plain/internal/files/uploadedfile.py,sha256=RaMeOMMB5LhH_QTEda9fGcI4kEg5CgCLE3kT
70
70
  plain/internal/files/uploadhandler.py,sha256=zUEMePuCsoaukRMCy5yBvMHaeOBageUw2sLBHsXpmew,7982
71
71
  plain/internal/files/utils.py,sha256=9aWCkGGGRdZLbI921IOgeUOD9zxx4FgpWCzXvq0lMXU,2871
72
72
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- plain/internal/handlers/base.py,sha256=1xz-9esmx_-2mlUSEjtr55FXxjeExbVVpOdo88_2eAw,6529
73
+ plain/internal/handlers/base.py,sha256=nAK2Wfw1-f3fbsCp8w8zzHwl-Fb8sj9CXWPyMM0aLbk,6551
74
74
  plain/internal/handlers/exception.py,sha256=9Qf9dfQANuaeNx9-DMFJzg3Y3un61NicxfK7YnK3RTk,5226
75
- plain/internal/handlers/wsgi.py,sha256=UnQ1wJSA6zIY8lWVZYlirtRui-VcjxXocbPgJOtG7KQ,8863
75
+ plain/internal/handlers/wsgi.py,sha256=d2Hcs4fzTSir6OvtpVromTw0RmmFAqTL4_EaBjoHtNU,8919
76
76
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  plain/internal/middleware/headers.py,sha256=WM46oSTlmPsysbstOUoQLV8vsfGxbT9UpjYqEsw0FFQ,1200
78
78
  plain/internal/middleware/hosts.py,sha256=veh42e1JRNwegP4dVx_urQroEC857YAKt3ThDHyr9Rc,6017
@@ -88,7 +88,7 @@ plain/logs/utils.py,sha256=BuHFynr9Oy8R7LzN3WycBvDY1lNX8tAxJ3TBsnchb0k,1628
88
88
  plain/packages/README.md,sha256=iNqMtwFDVNf2TqKUzLKQW5Y4_GsssmdB4cVerzu27Ro,2674
89
89
  plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
90
90
  plain/packages/config.py,sha256=dxs_i-z6noQF_6j3lq11mhnQ1Bj10u2CXTJY0JgeQgc,3166
91
- plain/packages/registry.py,sha256=hIBF9JYgidRgfsAEOCwd2gtlWiRyeIiLmYQP1XYmJU8,9093
91
+ plain/packages/registry.py,sha256=bmqY1rQau4MRpbf6DXkUVlqF4XJ9Mz2awSi5E7tCGd4,9032
92
92
  plain/preflight/README.md,sha256=vR43F_ls81hRSo7J2NNZ4VOMoRaJ1bS5JwA6l4ez36g,1782
93
93
  plain/preflight/__init__.py,sha256=-uBIVLD1DlJUVypQsEcrOtaNAhECbOpKhyoz0c_WMhA,416
94
94
  plain/preflight/checks.py,sha256=kJcr-Hq5bsjKw1BUYR6r6nFg3Ecgrd1DS5SudUr4rSU,289
@@ -118,7 +118,7 @@ plain/templates/jinja/filters.py,sha256=g70cw1jzvYco2v-u4SeceOWBX_qxHI5k9AODMn8e
118
118
  plain/templates/jinja/globals.py,sha256=TXl6uObqis_KXYP-jL3SvwqhATaoc7_hU8_fwpBMXyk,570
119
119
  plain/test/README.md,sha256=tNzaVjma0sgowIrViJguCgVy8A2d8mUKApZO2RxTYyU,1140
120
120
  plain/test/__init__.py,sha256=MhNHtp7MYBl9kq-pMRGY11kJ6kU1I6vOkjNkit1TYRg,94
121
- plain/test/client.py,sha256=Gy98gsYXf6emaIC99YRqV9Z3wkJQp4uN0PXZLm1pOMs,29720
121
+ plain/test/client.py,sha256=wgmZOv8kyBDBDq6wamlFdVrmVCfWn5YD62dtARIpILA,31714
122
122
  plain/test/encoding.py,sha256=txj_FCbC4GxH-JCkopW5LaZz8cGsrKQiculjFkjkzuY,3372
123
123
  plain/test/exceptions.py,sha256=Cn4cauBelCiZPnbIXru-zKePXEQn-dit8M4v74C_dTk,492
124
124
  plain/urls/README.md,sha256=026RkCK6I0GdqK3RE2QBLcCLIsiwtyKxgI2F0KBX95E,3882
@@ -153,7 +153,7 @@ plain/utils/text.py,sha256=teav7elbqEtGnhKG3ajf-V9Hb-Gsg8uqDrogqWizqjI,10094
153
153
  plain/utils/timesince.py,sha256=a_-ZoPK_s3Pt998CW4rWp0clZ1XyK2x04hCqak2giII,5928
154
154
  plain/utils/timezone.py,sha256=M_I5yvs9NsHbtNBPJgHErvWw9vatzx4M96tRQs5gS3g,6823
155
155
  plain/utils/tree.py,sha256=rj_JpZ2kVD3UExWoKnsRdVCoRjvzkuVOONcHzREjSyw,4766
156
- plain/views/README.md,sha256=caUSKUhCSs5hdxHC5wIVzKkumPXiuNoOFRnIs3CUHfo,7215
156
+ plain/views/README.md,sha256=6mcoSQp60n8qgoIMNDQr29WThpi-NCj8EMqxPNwWpiE,7189
157
157
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
158
158
  plain/views/base.py,sha256=fk9zAY5BMVBeM45dWL7A9BMTdUi6eTFMeVDd5kBVdv8,4478
159
159
  plain/views/errors.py,sha256=tHD7MNnZcMyiQ46RMAnX1Ne3Zbbkr1zAiVfJyaaLtSQ,1447
@@ -162,8 +162,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
162
162
  plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
163
163
  plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
164
164
  plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
165
- plain-0.72.0.dist-info/METADATA,sha256=Do-bYyzKeMCcIh-WWtf1dN2OapU5J03g9bZkgonB2Q4,4488
166
- plain-0.72.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
167
- plain-0.72.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
168
- plain-0.72.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
169
- plain-0.72.0.dist-info/RECORD,,
165
+ plain-0.72.2.dist-info/METADATA,sha256=oNUbDDi3O3AiTeJuuZ_tPo1D5tBhEkzhNIligFBg-kE,4488
166
+ plain-0.72.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
167
+ plain-0.72.2.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
168
+ plain-0.72.2.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
169
+ plain-0.72.2.dist-info/RECORD,,
File without changes