plain.auth 0.20.0__py3-none-any.whl → 0.20.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/auth/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # plain-auth changelog
2
2
 
3
+ ## [0.20.2](https://github.com/dropseed/plain/releases/plain-auth@0.20.2) (2025-10-06)
4
+
5
+ ### What's changed
6
+
7
+ - Added comprehensive type annotations across the entire package for improved IDE support and type checking ([786c1db](https://github.com/dropseed/plain/commit/786c1dbdbd))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
13
+ ## [0.20.1](https://github.com/dropseed/plain/releases/plain-auth@0.20.1) (2025-10-02)
14
+
15
+ ### What's changed
16
+
17
+ - Updated README documentation to use `get_request_user()` and `get_current_user()` instead of `request.user` ([f6278d9](https://github.com/dropseed/plain/commit/f6278d9bb4))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - No changes required
22
+
3
23
  ## [0.20.0](https://github.com/dropseed/plain/releases/plain-auth@0.20.0) (2025-10-02)
4
24
 
5
25
  ### What's changed
plain/auth/README.md CHANGED
@@ -17,8 +17,11 @@ The `plain.auth` package provides user authentication and authorization for Plai
17
17
 
18
18
  ```python
19
19
  # In a view
20
- if request.user:
21
- print(f"Hello, {request.user.email}!")
20
+ from plain.auth import get_request_user
21
+
22
+ user = get_request_user(request)
23
+ if user:
24
+ print(f"Hello, {user.email}!")
22
25
  else:
23
26
  print("You are not logged in.")
24
27
  ```
@@ -33,7 +36,7 @@ class ProfileView(AuthViewMixin, View):
33
36
  login_required = True
34
37
 
35
38
  def get(self):
36
- return f"Welcome, {self.request.user.email}!"
39
+ return f"Welcome, {self.user.email}!"
37
40
  ```
38
41
 
39
42
  ## Authentication setup
@@ -108,23 +111,24 @@ urlpatterns = [
108
111
 
109
112
  ## Checking if a user is logged in
110
113
 
111
- A `request.user` will either be `None` or point to an instance of your `AUTH_USER_MODEL`.
112
-
113
- In templates:
114
+ In templates, use the `get_current_user()` function:
114
115
 
115
116
  ```html
116
- {% if request.user %}
117
- <p>Hello, {{ request.user.email }}!</p>
117
+ {% if get_current_user() %}
118
+ <p>Hello, {{ get_current_user().email }}!</p>
118
119
  {% else %}
119
120
  <p>You are not logged in.</p>
120
121
  {% endif %}
121
122
  ```
122
123
 
123
- In Python code:
124
+ In Python code, use `get_request_user()`:
124
125
 
125
126
  ```python
126
- if request.user:
127
- print(f"Hello, {request.user.email}!")
127
+ from plain.auth import get_request_user
128
+
129
+ user = get_request_user(request)
130
+ if user:
131
+ print(f"Hello, {user.email}!")
128
132
  else:
129
133
  print("You are not logged in.")
130
134
  ```
@@ -151,7 +155,7 @@ class AdminOnlyView(AuthViewMixin, View):
151
155
  class CustomPermissionView(AuthViewMixin, View):
152
156
  def check_auth(self):
153
157
  super().check_auth()
154
- if not self.request.user.is_special:
158
+ if not self.user.is_special:
155
159
  raise PermissionDenied("You're not special!")
156
160
  ```
157
161
 
plain/auth/requests.py CHANGED
@@ -1,24 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
  from weakref import WeakKeyDictionary
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from plain.http import Request
8
8
 
9
- from .sessions import get_user_model
9
+ _request_users: WeakKeyDictionary[Request, Any | None] = WeakKeyDictionary()
10
10
 
11
- User = get_user_model()
12
11
 
13
- _request_users: WeakKeyDictionary[Request, User | None] = WeakKeyDictionary()
14
-
15
-
16
- def set_request_user(request: Request, user: User | None) -> None:
12
+ def set_request_user(request: Request, user: Any | None) -> None:
17
13
  """Store the authenticated user for this request."""
18
14
  _request_users[request] = user
19
15
 
20
16
 
21
- def get_request_user(request: Request) -> User | None:
17
+ def get_request_user(request: Request) -> Any | None:
22
18
  """
23
19
  Get the authenticated user for this request, if any.
24
20
 
plain/auth/sessions.py CHANGED
@@ -1,4 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import hmac
4
+ from collections.abc import Generator
5
+ from typing import TYPE_CHECKING, Any
2
6
 
3
7
  from plain.exceptions import ImproperlyConfigured
4
8
  from plain.models import models_registry
@@ -9,18 +13,21 @@ from plain.utils.encoding import force_bytes
9
13
 
10
14
  from .requests import get_request_user, set_request_user
11
15
 
16
+ if TYPE_CHECKING:
17
+ from plain.http import Request
18
+
12
19
  USER_ID_SESSION_KEY = "_auth_user_id"
13
20
  USER_HASH_SESSION_KEY = "_auth_user_hash"
14
21
 
15
22
 
16
- def get_session_auth_hash(user):
23
+ def get_session_auth_hash(user: Any) -> str:
17
24
  """
18
25
  Return an HMAC of the password field.
19
26
  """
20
27
  return _get_session_auth_hash(user)
21
28
 
22
29
 
23
- def update_session_auth_hash(request, user):
30
+ def update_session_auth_hash(request: Request, user: Any) -> None:
24
31
  """
25
32
  Updating a user's password (for example) logs out all sessions for the user.
26
33
 
@@ -36,12 +43,12 @@ def update_session_auth_hash(request, user):
36
43
  session[USER_HASH_SESSION_KEY] = get_session_auth_hash(user)
37
44
 
38
45
 
39
- def get_session_auth_fallback_hash(user):
46
+ def get_session_auth_fallback_hash(user: Any) -> Generator[str, None, None]:
40
47
  for fallback_secret in settings.SECRET_KEY_FALLBACKS:
41
48
  yield _get_session_auth_hash(user, secret=fallback_secret)
42
49
 
43
50
 
44
- def _get_session_auth_hash(user, secret=None):
51
+ def _get_session_auth_hash(user: Any, secret: str | None = None) -> str:
45
52
  key_salt = "plain.auth.get_session_auth_hash"
46
53
  return salted_hmac(
47
54
  key_salt,
@@ -51,7 +58,7 @@ def _get_session_auth_hash(user, secret=None):
51
58
  ).hexdigest()
52
59
 
53
60
 
54
- def login(request, user):
61
+ def login(request: Request, user: Any) -> None:
55
62
  """
56
63
  Persist a user id and a backend in the request. This way a user doesn't
57
64
  have to reauthenticate on every request. Note that data set during
@@ -87,7 +94,7 @@ def login(request, user):
87
94
  set_request_user(request, user)
88
95
 
89
96
 
90
- def logout(request):
97
+ def logout(request: Request) -> None:
91
98
  """
92
99
  Remove the authenticated user's ID from the request and flush their session
93
100
  data.
@@ -99,7 +106,7 @@ def logout(request):
99
106
  set_request_user(request, None)
100
107
 
101
108
 
102
- def get_user_model():
109
+ def get_user_model() -> type[Any]:
103
110
  """
104
111
  Return the User model that is active in this project.
105
112
  """
@@ -115,7 +122,7 @@ def get_user_model():
115
122
  )
116
123
 
117
124
 
118
- def get_user(request):
125
+ def get_user(request: Request) -> Any | None:
119
126
  """
120
127
  Return the user model instance associated with the given request session.
121
128
  If no user is retrieved, return None.
plain/auth/templates.py CHANGED
@@ -1,13 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
1
5
  from jinja2 import pass_context
2
6
 
3
7
  from plain.templates import register_template_global
4
8
 
5
9
  from .requests import get_request_user
6
10
 
11
+ if TYPE_CHECKING:
12
+ from jinja2.runtime import Context
13
+
7
14
 
8
15
  @register_template_global
9
16
  @pass_context
10
- def get_current_user(context):
17
+ def get_current_user(context: Context) -> Any | None:
11
18
  """Get the authenticated user for the current request."""
12
19
  request = context.get("request")
13
20
  assert request is not None, "No request in template context"
plain/auth/test.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from http.cookies import SimpleCookie
4
+ from typing import TYPE_CHECKING, Any
2
5
 
3
6
  from plain.http.request import Request
4
7
  from plain.runtime import settings
@@ -8,8 +11,11 @@ from plain.sessions.requests import get_request_session, set_request_session
8
11
  from .requests import set_request_user
9
12
  from .sessions import get_user, login, logout
10
13
 
14
+ if TYPE_CHECKING:
15
+ from plain.test.client import Client
16
+
11
17
 
12
- def login_client(client, user):
18
+ def login_client(client: Client, user: Any) -> None:
13
19
  """Log a user into a test client."""
14
20
  request = Request()
15
21
  if client.session:
@@ -32,7 +38,7 @@ def login_client(client, user):
32
38
  client.cookies[session_cookie].update(cookie_data)
33
39
 
34
40
 
35
- def logout_client(client):
41
+ def logout_client(client: Client) -> None:
36
42
  """Log out a user from a test client."""
37
43
  request = Request()
38
44
  if client.session:
plain/auth/utils.py CHANGED
@@ -1,8 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
1
5
  from plain.urls import NoReverseMatch, reverse
2
6
  from plain.utils.functional import Promise
3
7
 
4
8
 
5
- def resolve_url(to, *args, **kwargs):
9
+ def resolve_url(to: Any, *args: Any, **kwargs: Any) -> str:
6
10
  """
7
11
  Return a URL appropriate for the arguments passed.
8
12
 
plain/auth/views.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from functools import cached_property
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Any
5
5
  from urllib.parse import urlparse, urlunparse
6
6
 
7
7
  from plain.exceptions import PermissionDenied
@@ -23,13 +23,9 @@ from .utils import resolve_url
23
23
  if TYPE_CHECKING:
24
24
  from plain.http import Request
25
25
 
26
- from .sessions import get_user_model
27
-
28
- User = get_user_model()
29
-
30
26
 
31
27
  class LoginRequired(Exception):
32
- def __init__(self, login_url=None, redirect_field_name="next"):
28
+ def __init__(self, login_url: str | None = None, redirect_field_name: str = "next"):
33
29
  self.login_url = login_url or settings.AUTH_LOGIN_URL
34
30
  self.redirect_field_name = redirect_field_name
35
31
 
@@ -42,7 +38,7 @@ class AuthViewMixin(SessionViewMixin):
42
38
  request: Request
43
39
 
44
40
  @cached_property
45
- def user(self) -> User | None:
41
+ def user(self) -> Any | None:
46
42
  """Get the authenticated user for this request."""
47
43
  from .requests import get_request_user
48
44
 
@@ -116,12 +112,14 @@ class AuthViewMixin(SessionViewMixin):
116
112
 
117
113
 
118
114
  class LogoutView(View):
119
- def post(self):
115
+ def post(self) -> ResponseRedirect:
120
116
  logout(self.request)
121
117
  return ResponseRedirect("/")
122
118
 
123
119
 
124
- def redirect_to_login(next, login_url=None, redirect_field_name="next"):
120
+ def redirect_to_login(
121
+ next: str, login_url: str | None = None, redirect_field_name: str = "next"
122
+ ) -> ResponseRedirect:
125
123
  """
126
124
  Redirect the user to the login page, passing the given 'next' page.
127
125
  """
@@ -133,4 +131,4 @@ def redirect_to_login(next, login_url=None, redirect_field_name="next"):
133
131
  querystring[redirect_field_name] = next
134
132
  login_url_parts[4] = querystring.urlencode(safe="/")
135
133
 
136
- return ResponseRedirect(urlunparse(login_url_parts))
134
+ return ResponseRedirect(str(urlunparse(login_url_parts)))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.auth
3
- Version: 0.20.0
3
+ Version: 0.20.2
4
4
  Summary: Add users to your app and decide what they can access.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -29,8 +29,11 @@ The `plain.auth` package provides user authentication and authorization for Plai
29
29
 
30
30
  ```python
31
31
  # In a view
32
- if request.user:
33
- print(f"Hello, {request.user.email}!")
32
+ from plain.auth import get_request_user
33
+
34
+ user = get_request_user(request)
35
+ if user:
36
+ print(f"Hello, {user.email}!")
34
37
  else:
35
38
  print("You are not logged in.")
36
39
  ```
@@ -45,7 +48,7 @@ class ProfileView(AuthViewMixin, View):
45
48
  login_required = True
46
49
 
47
50
  def get(self):
48
- return f"Welcome, {self.request.user.email}!"
51
+ return f"Welcome, {self.user.email}!"
49
52
  ```
50
53
 
51
54
  ## Authentication setup
@@ -120,23 +123,24 @@ urlpatterns = [
120
123
 
121
124
  ## Checking if a user is logged in
122
125
 
123
- A `request.user` will either be `None` or point to an instance of your `AUTH_USER_MODEL`.
124
-
125
- In templates:
126
+ In templates, use the `get_current_user()` function:
126
127
 
127
128
  ```html
128
- {% if request.user %}
129
- <p>Hello, {{ request.user.email }}!</p>
129
+ {% if get_current_user() %}
130
+ <p>Hello, {{ get_current_user().email }}!</p>
130
131
  {% else %}
131
132
  <p>You are not logged in.</p>
132
133
  {% endif %}
133
134
  ```
134
135
 
135
- In Python code:
136
+ In Python code, use `get_request_user()`:
136
137
 
137
138
  ```python
138
- if request.user:
139
- print(f"Hello, {request.user.email}!")
139
+ from plain.auth import get_request_user
140
+
141
+ user = get_request_user(request)
142
+ if user:
143
+ print(f"Hello, {user.email}!")
140
144
  else:
141
145
  print("You are not logged in.")
142
146
  ```
@@ -163,7 +167,7 @@ class AdminOnlyView(AuthViewMixin, View):
163
167
  class CustomPermissionView(AuthViewMixin, View):
164
168
  def check_auth(self):
165
169
  super().check_auth()
166
- if not self.request.user.is_special:
170
+ if not self.user.is_special:
167
171
  raise PermissionDenied("You're not special!")
168
172
  ```
169
173
 
@@ -0,0 +1,14 @@
1
+ plain/auth/CHANGELOG.md,sha256=KEwd7C-ZHFlVY2Rv4G8tm_KFU6iPN-3-ZBs-W479_wA,4864
2
+ plain/auth/README.md,sha256=I1SeOyrBnF0GAjD7T0k5OlZ2bSHz9---nElinaobewc,4030
3
+ plain/auth/__init__.py,sha256=CrOsS74CPGN1nPTTfie13mPgdyVLRyZ1YwDPIA77uaA,179
4
+ plain/auth/default_settings.py,sha256=65VzDn3j61OMn78Lg6Zuds4A8QKzJJ_0G9KoFqAOIRo,466
5
+ plain/auth/requests.py,sha256=jUlMTOWFt0qfDF_uVkOqaP9rZiCLrAofsmirCwyRGEE,880
6
+ plain/auth/sessions.py,sha256=xDp1EiB0cV5w3L5XioqO8vs77wnF8cvreExms3-e744,6015
7
+ plain/auth/templates.py,sha256=bOMPfLrf8SSxOSWrKTkqKGaK3aNBTLxe62HJulghutU,556
8
+ plain/auth/test.py,sha256=SHawhwarJEMVfaLkjpiuFVSWZoGIMwhzXreU_T1zvCE,1599
9
+ plain/auth/utils.py,sha256=9kKWh1QqxA8Esct-jBvTCdjBYOHpO_Tg1YeV9WxYmxg,1362
10
+ plain/auth/views.py,sha256=vWLMeykTrA5LtA179uE7w-BY9Kv25r1VScLHooDvZUA,4792
11
+ plain_auth-0.20.2.dist-info/METADATA,sha256=BE1BOkDeM_Nw6Ym8w3VanuGn7uHN8wwnHkmuYhcEVBU,4390
12
+ plain_auth-0.20.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ plain_auth-0.20.2.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
+ plain_auth-0.20.2.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- plain/auth/CHANGELOG.md,sha256=IyxuPyEHvnwqEu6GKtUEOtlu1iuLg5Xux6610lrRAIU,4200
2
- plain/auth/README.md,sha256=Kr3pW6XDd9sp-b8DOH2uKRqCqrR8MYB98qRUxeiwn0k,3944
3
- plain/auth/__init__.py,sha256=CrOsS74CPGN1nPTTfie13mPgdyVLRyZ1YwDPIA77uaA,179
4
- plain/auth/default_settings.py,sha256=65VzDn3j61OMn78Lg6Zuds4A8QKzJJ_0G9KoFqAOIRo,466
5
- plain/auth/requests.py,sha256=f8QUpqHueoTYnL2ITgRXxLFYLSxra9jW3p9pjFgj0B0,949
6
- plain/auth/sessions.py,sha256=Pz27K2wFfij0H0nGg_2rq0WJpcgTWW22bG0YvHM-dfc,5679
7
- plain/auth/templates.py,sha256=MQ9vdxE2fBXN-F043g8EGm93s6HRvEMtGHrFcbAr3Fs,400
8
- plain/auth/test.py,sha256=bLxQEp1mM4t9b-bSSq2u9_Z07Wkl--7U_mSBb9VV7BU,1428
9
- plain/auth/utils.py,sha256=eEON0Mo928l-aW5tqBuoTdVke8aP4majxVtAFoLroSE,1280
10
- plain/auth/views.py,sha256=Gm91sB_Z47h-8SwxCMKVKl57FSjed_AM5fdeaiPmz-E,4761
11
- plain_auth-0.20.0.dist-info/METADATA,sha256=9LQj-14o1wKsAbKp3xTd09vmjuJTzYvcD4xjiUqGRqY,4304
12
- plain_auth-0.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- plain_auth-0.20.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
- plain_auth-0.20.0.dist-info/RECORD,,