core-framework 0.12.5__tar.gz → 0.12.7__tar.gz

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 (131) hide show
  1. {core_framework-0.12.5 → core_framework-0.12.7}/PKG-INFO +1 -1
  2. {core_framework-0.12.5 → core_framework-0.12.7}/core/__init__.py +1 -1
  3. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/__init__.py +15 -0
  4. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/backends.py +5 -5
  5. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/decorators.py +28 -6
  6. core_framework-0.12.7/core/auth/helpers.py +104 -0
  7. {core_framework-0.12.5 → core_framework-0.12.7}/core/cli/main.py +6 -4
  8. {core_framework-0.12.5 → core_framework-0.12.7}/core/dependencies.py +3 -1
  9. {core_framework-0.12.5 → core_framework-0.12.7}/core/permissions.py +29 -6
  10. {core_framework-0.12.5 → core_framework-0.12.7}/core/tenancy.py +3 -2
  11. {core_framework-0.12.5 → core_framework-0.12.7}/pyproject.toml +1 -1
  12. core_framework-0.12.7/tests/test_auth_helpers.py +206 -0
  13. core_framework-0.12.7/tests/test_imports.py +263 -0
  14. core_framework-0.12.7/tests/test_permissions.py +373 -0
  15. {core_framework-0.12.5 → core_framework-0.12.7}/.gitignore +0 -0
  16. {core_framework-0.12.5 → core_framework-0.12.7}/README.md +0 -0
  17. {core_framework-0.12.5 → core_framework-0.12.7}/core/app.py +0 -0
  18. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/base.py +0 -0
  19. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/hashers.py +0 -0
  20. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/middleware.py +0 -0
  21. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/models.py +0 -0
  22. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/permissions.py +0 -0
  23. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/schemas.py +0 -0
  24. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/tokens.py +0 -0
  25. {core_framework-0.12.5 → core_framework-0.12.7}/core/auth/views.py +0 -0
  26. {core_framework-0.12.5 → core_framework-0.12.7}/core/choices.py +0 -0
  27. {core_framework-0.12.5 → core_framework-0.12.7}/core/cli/__init__.py +0 -0
  28. {core_framework-0.12.5 → core_framework-0.12.7}/core/config.py +0 -0
  29. {core_framework-0.12.5 → core_framework-0.12.7}/core/database.py +0 -0
  30. {core_framework-0.12.5 → core_framework-0.12.7}/core/datetime.py +0 -0
  31. {core_framework-0.12.5 → core_framework-0.12.7}/core/deployment/__init__.py +0 -0
  32. {core_framework-0.12.5 → core_framework-0.12.7}/core/deployment/docker.py +0 -0
  33. {core_framework-0.12.5 → core_framework-0.12.7}/core/deployment/kubernetes.py +0 -0
  34. {core_framework-0.12.5 → core_framework-0.12.7}/core/deployment/pm2.py +0 -0
  35. {core_framework-0.12.5 → core_framework-0.12.7}/core/exceptions.py +0 -0
  36. {core_framework-0.12.5 → core_framework-0.12.7}/core/fields.py +0 -0
  37. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/__init__.py +0 -0
  38. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/avro.py +0 -0
  39. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/base.py +0 -0
  40. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/config.py +0 -0
  41. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/confluent/__init__.py +0 -0
  42. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/confluent/consumer.py +0 -0
  43. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/confluent/producer.py +0 -0
  44. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/decorators.py +0 -0
  45. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/kafka/__init__.py +0 -0
  46. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/kafka/admin.py +0 -0
  47. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/kafka/broker.py +0 -0
  48. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/kafka/consumer.py +0 -0
  49. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/kafka/producer.py +0 -0
  50. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/rabbitmq/__init__.py +0 -0
  51. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/rabbitmq/broker.py +0 -0
  52. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/rabbitmq/consumer.py +0 -0
  53. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/rabbitmq/producer.py +0 -0
  54. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/redis/__init__.py +0 -0
  55. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/redis/broker.py +0 -0
  56. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/redis/consumer.py +0 -0
  57. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/redis/producer.py +0 -0
  58. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/registry.py +0 -0
  59. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/topics.py +0 -0
  60. {core_framework-0.12.5 → core_framework-0.12.7}/core/messaging/workers.py +0 -0
  61. {core_framework-0.12.5 → core_framework-0.12.7}/core/middleware.py +0 -0
  62. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/__init__.py +0 -0
  63. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/analyzer.py +0 -0
  64. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/cli.py +0 -0
  65. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/engine.py +0 -0
  66. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/migration.py +0 -0
  67. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/operations.py +0 -0
  68. {core_framework-0.12.5 → core_framework-0.12.7}/core/migrations/state.py +0 -0
  69. {core_framework-0.12.5 → core_framework-0.12.7}/core/models.py +0 -0
  70. {core_framework-0.12.5 → core_framework-0.12.7}/core/querysets.py +0 -0
  71. {core_framework-0.12.5 → core_framework-0.12.7}/core/relations.py +0 -0
  72. {core_framework-0.12.5 → core_framework-0.12.7}/core/routing.py +0 -0
  73. {core_framework-0.12.5 → core_framework-0.12.7}/core/serializers.py +0 -0
  74. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/__init__.py +0 -0
  75. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/base.py +0 -0
  76. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/config.py +0 -0
  77. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/decorators.py +0 -0
  78. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/registry.py +0 -0
  79. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/scheduler.py +0 -0
  80. {core_framework-0.12.5 → core_framework-0.12.7}/core/tasks/worker.py +0 -0
  81. {core_framework-0.12.5 → core_framework-0.12.7}/core/validators.py +0 -0
  82. {core_framework-0.12.5 → core_framework-0.12.7}/core/views.py +0 -0
  83. {core_framework-0.12.5 → core_framework-0.12.7}/docs/01-quickstart.md +0 -0
  84. {core_framework-0.12.5 → core_framework-0.12.7}/docs/02-viewsets.md +0 -0
  85. {core_framework-0.12.5 → core_framework-0.12.7}/docs/03-authentication.md +0 -0
  86. {core_framework-0.12.5 → core_framework-0.12.7}/docs/04-messaging.md +0 -0
  87. {core_framework-0.12.5 → core_framework-0.12.7}/docs/05-multi-service.md +0 -0
  88. {core_framework-0.12.5 → core_framework-0.12.7}/docs/06-tasks.md +0 -0
  89. {core_framework-0.12.5 → core_framework-0.12.7}/docs/07-deployment.md +0 -0
  90. {core_framework-0.12.5 → core_framework-0.12.7}/docs/08-complete-example.md +0 -0
  91. {core_framework-0.12.5 → core_framework-0.12.7}/docs/09-settings.md +0 -0
  92. {core_framework-0.12.5 → core_framework-0.12.7}/docs/10-migrations.md +0 -0
  93. {core_framework-0.12.5 → core_framework-0.12.7}/docs/11-permissions.md +0 -0
  94. {core_framework-0.12.5 → core_framework-0.12.7}/docs/12-auth-backends.md +0 -0
  95. {core_framework-0.12.5 → core_framework-0.12.7}/docs/13-validators.md +0 -0
  96. {core_framework-0.12.5 → core_framework-0.12.7}/docs/14-querysets.md +0 -0
  97. {core_framework-0.12.5 → core_framework-0.12.7}/docs/15-routing.md +0 -0
  98. {core_framework-0.12.5 → core_framework-0.12.7}/docs/16-serializers.md +0 -0
  99. {core_framework-0.12.5 → core_framework-0.12.7}/docs/17-datetime.md +0 -0
  100. {core_framework-0.12.5 → core_framework-0.12.7}/docs/18-dependencies.md +0 -0
  101. {core_framework-0.12.5 → core_framework-0.12.7}/docs/19-views.md +0 -0
  102. {core_framework-0.12.5 → core_framework-0.12.7}/docs/20-fields.md +0 -0
  103. {core_framework-0.12.5 → core_framework-0.12.7}/docs/21-tenancy.md +0 -0
  104. {core_framework-0.12.5 → core_framework-0.12.7}/docs/22-replicas.md +0 -0
  105. {core_framework-0.12.5 → core_framework-0.12.7}/docs/23-soft-delete.md +0 -0
  106. {core_framework-0.12.5 → core_framework-0.12.7}/docs/24-relations.md +0 -0
  107. {core_framework-0.12.5 → core_framework-0.12.7}/docs/25-exceptions.md +0 -0
  108. {core_framework-0.12.5 → core_framework-0.12.7}/docs/26-choices.md +0 -0
  109. {core_framework-0.12.5 → core_framework-0.12.7}/docs/27-workers.md +0 -0
  110. {core_framework-0.12.5 → core_framework-0.12.7}/docs/28-avro.md +0 -0
  111. {core_framework-0.12.5 → core_framework-0.12.7}/docs/29-topics.md +0 -0
  112. {core_framework-0.12.5 → core_framework-0.12.7}/docs/30-changelog-0.12.2.md +0 -0
  113. {core_framework-0.12.5 → core_framework-0.12.7}/docs/31-middleware.md +0 -0
  114. {core_framework-0.12.5 → core_framework-0.12.7}/docs/32-migration-guide-0.12.2.md +0 -0
  115. {core_framework-0.12.5 → core_framework-0.12.7}/docs/33-changelog-0.12.3.md +0 -0
  116. {core_framework-0.12.5 → core_framework-0.12.7}/docs/99-faq-troubleshooting.md +0 -0
  117. {core_framework-0.12.5 → core_framework-0.12.7}/docs/GUIDE.md +0 -0
  118. {core_framework-0.12.5 → core_framework-0.12.7}/docs/README.md +0 -0
  119. {core_framework-0.12.5 → core_framework-0.12.7}/example/__init__.py +0 -0
  120. {core_framework-0.12.5 → core_framework-0.12.7}/example/app.py +0 -0
  121. {core_framework-0.12.5 → core_framework-0.12.7}/example/auth.py +0 -0
  122. {core_framework-0.12.5 → core_framework-0.12.7}/example/models.py +0 -0
  123. {core_framework-0.12.5 → core_framework-0.12.7}/example/schemas.py +0 -0
  124. {core_framework-0.12.5 → core_framework-0.12.7}/example/views.py +0 -0
  125. {core_framework-0.12.5 → core_framework-0.12.7}/libs/__init__.py +0 -0
  126. {core_framework-0.12.5 → core_framework-0.12.7}/main.py +0 -0
  127. {core_framework-0.12.5 → core_framework-0.12.7}/tests/__init__.py +0 -0
  128. {core_framework-0.12.5 → core_framework-0.12.7}/tests/conftest.py +0 -0
  129. {core_framework-0.12.5 → core_framework-0.12.7}/tests/test_models.py +0 -0
  130. {core_framework-0.12.5 → core_framework-0.12.7}/tests/test_querysets.py +0 -0
  131. {core_framework-0.12.5 → core_framework-0.12.7}/tests/test_serializers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.5
3
+ Version: 0.12.7
4
4
  Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
5
5
  Project-URL: Homepage, https://github.com/SorPuti/core-framework
6
6
  Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.5"
281
+ __version__ = "0.12.7"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
@@ -121,9 +121,18 @@ from core.auth.views import (
121
121
  from core.auth.middleware import (
122
122
  AuthenticationMiddleware,
123
123
  OptionalAuthenticationMiddleware,
124
+ JWTAuthBackend,
125
+ AuthenticatedUser,
124
126
  ensure_auth_middleware,
125
127
  )
126
128
 
129
+ # Helper functions for consistent user access
130
+ from core.auth.helpers import (
131
+ get_request_user,
132
+ is_authenticated,
133
+ set_request_user,
134
+ )
135
+
127
136
  __all__ = [
128
137
  # Base
129
138
  "AuthBackend",
@@ -196,5 +205,11 @@ __all__ = [
196
205
  # Middleware
197
206
  "AuthenticationMiddleware",
198
207
  "OptionalAuthenticationMiddleware",
208
+ "JWTAuthBackend",
209
+ "AuthenticatedUser",
199
210
  "ensure_auth_middleware",
211
+ # Helpers
212
+ "get_request_user",
213
+ "is_authenticated",
214
+ "set_request_user",
200
215
  ]
@@ -161,14 +161,14 @@ class ModelBackend(AuthBackend):
161
161
 
162
162
  async def login(self, request: "Request", user: Any) -> None:
163
163
  """Executa ações pós-login."""
164
- # Armazena usuário no request.state
165
- request.state.user = user
164
+ # Armazena usuário no request.state (for backward compatibility)
165
+ from core.auth.helpers import set_request_user
166
+ set_request_user(request, user)
166
167
 
167
168
  async def logout(self, request: "Request", user: Any) -> None:
168
169
  """Executa ações de logout."""
169
- # Remove usuário do request.state
170
- if hasattr(request.state, "user"):
171
- request.state.user = None
170
+ from core.auth.helpers import set_request_user
171
+ set_request_user(request, None)
172
172
 
173
173
 
174
174
  class TokenAuthBackend(AuthBackend):
@@ -29,6 +29,28 @@ if TYPE_CHECKING:
29
29
  pass
30
30
 
31
31
 
32
+ def _get_user(request: Request) -> Any | None:
33
+ """
34
+ Get authenticated user from request.
35
+
36
+ Internal helper to avoid circular imports.
37
+ Checks both Starlette and legacy patterns.
38
+ """
39
+ # Pattern 1: request.user (Starlette AuthenticationMiddleware)
40
+ user = getattr(request, "user", None)
41
+ if user is not None:
42
+ if getattr(user, "is_authenticated", False):
43
+ if hasattr(user, "_user"):
44
+ return user._user
45
+ return user
46
+
47
+ # Pattern 2: request.state.user (legacy)
48
+ if hasattr(request, "state"):
49
+ return getattr(request.state, "user", None)
50
+
51
+ return None
52
+
53
+
32
54
  # =============================================================================
33
55
  # Classes de Permissão (para uso em ViewSets)
34
56
  # =============================================================================
@@ -70,7 +92,7 @@ class HasPermission(PermissionBase):
70
92
  request: Request,
71
93
  view: Any = None,
72
94
  ) -> bool:
73
- user = getattr(request.state, "user", None)
95
+ user = _get_user(request)
74
96
 
75
97
  if user is None:
76
98
  return False
@@ -135,7 +157,7 @@ class IsInGroup(PermissionBase):
135
157
  request: Request,
136
158
  view: Any = None,
137
159
  ) -> bool:
138
- user = getattr(request.state, "user", None)
160
+ user = _get_user(request)
139
161
 
140
162
  if user is None:
141
163
  return False
@@ -179,7 +201,7 @@ class IsSuperuser(PermissionBase):
179
201
  request: Request,
180
202
  view: Any = None,
181
203
  ) -> bool:
182
- user = getattr(request.state, "user", None)
204
+ user = _get_user(request)
183
205
 
184
206
  if user is None:
185
207
  return False
@@ -207,7 +229,7 @@ class IsStaff(PermissionBase):
207
229
  request: Request,
208
230
  view: Any = None,
209
231
  ) -> bool:
210
- user = getattr(request.state, "user", None)
232
+ user = _get_user(request)
211
233
 
212
234
  if user is None:
213
235
  return False
@@ -235,7 +257,7 @@ class IsActive(PermissionBase):
235
257
  request: Request,
236
258
  view: Any = None,
237
259
  ) -> bool:
238
- user = getattr(request.state, "user", None)
260
+ user = _get_user(request)
239
261
 
240
262
  if user is None:
241
263
  return False
@@ -371,7 +393,7 @@ def login_required():
371
393
  ...
372
394
  """
373
395
  async def check(request: Request):
374
- user = getattr(request.state, "user", None)
396
+ user = _get_user(request)
375
397
 
376
398
  if user is None:
377
399
  raise HTTPException(
@@ -0,0 +1,104 @@
1
+ """
2
+ Helper functions for authentication.
3
+
4
+ Provides consistent user access across the framework, supporting both:
5
+ - request.user (Starlette AuthenticationMiddleware pattern - preferred)
6
+ - request.state.user (legacy pattern - backward compatibility)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from starlette.requests import Request
16
+
17
+ logger = logging.getLogger("core.auth")
18
+
19
+
20
+ def get_request_user(request: "Request") -> Any | None:
21
+ """
22
+ Get authenticated user from request.
23
+
24
+ Checks both patterns for compatibility:
25
+ 1. request.user (Starlette AuthenticationMiddleware - preferred)
26
+ 2. request.state.user (legacy pattern)
27
+
28
+ Args:
29
+ request: The Starlette/FastAPI request object
30
+
31
+ Returns:
32
+ The authenticated user model or None if not authenticated
33
+
34
+ Example:
35
+ from core.auth.helpers import get_request_user
36
+
37
+ async def my_view(request: Request):
38
+ user = get_request_user(request)
39
+ if user is None:
40
+ raise HTTPException(401, "Not authenticated")
41
+ """
42
+ # Pattern 1: request.user (Starlette AuthenticationMiddleware)
43
+ user = getattr(request, "user", None)
44
+ if user is not None:
45
+ # Check if it's an authenticated user (has is_authenticated = True)
46
+ if getattr(user, "is_authenticated", False):
47
+ # If it's our AuthenticatedUser wrapper, return the underlying model
48
+ if hasattr(user, "_user"):
49
+ return user._user
50
+ return user
51
+
52
+ # Pattern 2: request.state.user (legacy)
53
+ user = getattr(request.state, "user", None) if hasattr(request, "state") else None
54
+
55
+ return user
56
+
57
+
58
+ def is_authenticated(request: "Request") -> bool:
59
+ """
60
+ Check if request has an authenticated user.
61
+
62
+ Args:
63
+ request: The Starlette/FastAPI request object
64
+
65
+ Returns:
66
+ True if authenticated, False otherwise
67
+ """
68
+ # Pattern 1: request.user with is_authenticated
69
+ user = getattr(request, "user", None)
70
+ if user is not None and getattr(user, "is_authenticated", False):
71
+ return True
72
+
73
+ # Pattern 2: request.state.user
74
+ if hasattr(request, "state"):
75
+ user = getattr(request.state, "user", None)
76
+ if user is not None:
77
+ return True
78
+
79
+ return False
80
+
81
+
82
+ def set_request_user(request: "Request", user: Any | None) -> None:
83
+ """
84
+ Set the authenticated user on the request.
85
+
86
+ Sets both patterns for maximum compatibility:
87
+ - request.state.user (for dependencies and legacy code)
88
+
89
+ Note: request.user is set by Starlette's AuthenticationMiddleware
90
+ via scope["user"] and cannot be set directly.
91
+
92
+ Args:
93
+ request: The Starlette/FastAPI request object
94
+ user: The user model or None
95
+ """
96
+ if hasattr(request, "state"):
97
+ request.state.user = user
98
+
99
+
100
+ __all__ = [
101
+ "get_request_user",
102
+ "is_authenticated",
103
+ "set_request_user",
104
+ ]
@@ -1039,9 +1039,10 @@ class AuthViewSet(ModelViewSet):
1039
1039
  Returns:
1040
1040
  Current user data
1041
1041
  """
1042
- # User is available via request.state.user (populated by auth middleware)
1042
+ # User is available via request.user (Starlette pattern) or request.state.user (legacy)
1043
1043
  # permission_classes=[IsAuthenticated] ensures user is authenticated
1044
- user = request.state.user
1044
+ from core.auth.helpers import get_request_user
1045
+ user = get_request_user(request)
1045
1046
 
1046
1047
  if user is None:
1047
1048
  raise HTTPException(status_code=401, detail="Authentication required")
@@ -1065,9 +1066,10 @@ class AuthViewSet(ModelViewSet):
1065
1066
  body = await request.json()
1066
1067
  data = ChangePasswordInput.model_validate(body)
1067
1068
 
1068
- # User is available via request.state.user (populated by auth middleware)
1069
+ # User is available via request.user (Starlette pattern) or request.state.user (legacy)
1069
1070
  # permission_classes=[IsAuthenticated] ensures user is authenticated
1070
- user = request.state.user
1071
+ from core.auth.helpers import get_request_user
1072
+ user = get_request_user(request)
1071
1073
 
1072
1074
  if user is None:
1073
1075
  raise HTTPException(status_code=401, detail="Authentication required")
@@ -395,12 +395,14 @@ async def get_request_context(request: Request) -> dict[str, Any]:
395
395
 
396
396
  Útil para logging e auditoria.
397
397
  """
398
+ from core.auth.helpers import get_request_user
399
+
398
400
  return {
399
401
  "method": request.method,
400
402
  "url": str(request.url),
401
403
  "client_ip": request.client.host if request.client else None,
402
404
  "user_agent": request.headers.get("user-agent"),
403
- "user": getattr(request.state, "user", None),
405
+ "user": get_request_user(request),
404
406
  }
405
407
 
406
408
 
@@ -19,6 +19,29 @@ if TYPE_CHECKING:
19
19
  from core.views import APIView
20
20
 
21
21
 
22
+ def _get_user(request: Request) -> Any | None:
23
+ """
24
+ Get authenticated user from request.
25
+
26
+ Internal helper to avoid circular imports.
27
+ Checks both Starlette and legacy patterns.
28
+ """
29
+ # Pattern 1: request.user (Starlette AuthenticationMiddleware)
30
+ user = getattr(request, "user", None)
31
+ if user is not None:
32
+ if getattr(user, "is_authenticated", False):
33
+ # If it's our AuthenticatedUser wrapper, return the underlying model
34
+ if hasattr(user, "_user"):
35
+ return user._user
36
+ return user
37
+
38
+ # Pattern 2: request.state.user (legacy)
39
+ if hasattr(request, "state"):
40
+ return getattr(request.state, "user", None)
41
+
42
+ return None
43
+
44
+
22
45
  class Permission(ABC):
23
46
  """
24
47
  Classe base para permissões.
@@ -32,7 +55,7 @@ class Permission(ABC):
32
55
  request: Request,
33
56
  view: APIView | None = None,
34
57
  ) -> bool:
35
- user = getattr(request.state, "user", None)
58
+ user = _get_user(request)
36
59
  return user is not None and user.is_admin
37
60
  """
38
61
 
@@ -214,7 +237,7 @@ class IsAuthenticated(Permission):
214
237
  request: Request,
215
238
  view: "APIView | None" = None,
216
239
  ) -> bool:
217
- user = getattr(request.state, "user", None)
240
+ user = _get_user(request)
218
241
  return user is not None
219
242
 
220
243
 
@@ -236,7 +259,7 @@ class IsAuthenticatedOrReadOnly(Permission):
236
259
  if request.method in self.SAFE_METHODS:
237
260
  return True
238
261
 
239
- user = getattr(request.state, "user", None)
262
+ user = _get_user(request)
240
263
  return user is not None
241
264
 
242
265
 
@@ -250,7 +273,7 @@ class IsAdmin(Permission):
250
273
  request: Request,
251
274
  view: "APIView | None" = None,
252
275
  ) -> bool:
253
- user = getattr(request.state, "user", None)
276
+ user = _get_user(request)
254
277
  if user is None:
255
278
  return False
256
279
 
@@ -285,7 +308,7 @@ class IsOwner(Permission):
285
308
  if obj is None:
286
309
  return True
287
310
 
288
- user = getattr(request.state, "user", None)
311
+ user = _get_user(request)
289
312
  if user is None:
290
313
  return False
291
314
 
@@ -318,7 +341,7 @@ class HasRole(Permission):
318
341
  request: Request,
319
342
  view: "APIView | None" = None,
320
343
  ) -> bool:
321
- user = getattr(request.state, "user", None)
344
+ user = _get_user(request)
322
345
  if user is None:
323
346
  return False
324
347
 
@@ -231,8 +231,9 @@ async def extract_tenant_from_request(
231
231
 
232
232
  Checks user, header, and query param in order.
233
233
  """
234
- # tenant_id = await extract_tenant_from_request(request)
235
- user = getattr(request.state, "user", None)
234
+ from core.auth.helpers import get_request_user
235
+
236
+ user = get_request_user(request)
236
237
  if user is not None:
237
238
  tenant_id = getattr(user, user_tenant_attr, None)
238
239
  if tenant_id is not None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "core-framework"
3
- version = "0.12.5"
3
+ version = "0.12.7"
4
4
  description = "Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema."
5
5
  requires-python = ">=3.10,<4.0"
6
6
  readme = "README.md"
@@ -0,0 +1,206 @@
1
+ """
2
+ Tests for auth helpers.
3
+
4
+ These tests validate the get_request_user helper works correctly
5
+ with both Starlette and legacy patterns.
6
+ """
7
+
8
+ import pytest
9
+ from unittest.mock import MagicMock, PropertyMock
10
+
11
+
12
+ class TestGetRequestUser:
13
+ """Test get_request_user helper function."""
14
+
15
+ def test_returns_none_when_no_user(self):
16
+ """Test returns None when no user is set."""
17
+ from core.auth.helpers import get_request_user
18
+
19
+ request = MagicMock()
20
+ request.user = None
21
+ request.state = MagicMock()
22
+ request.state.user = None
23
+
24
+ result = get_request_user(request)
25
+ assert result is None
26
+
27
+ def test_returns_user_from_starlette_pattern(self):
28
+ """Test returns user from request.user (Starlette pattern)."""
29
+ from core.auth.helpers import get_request_user
30
+
31
+ # Create a mock user that looks like AuthenticatedUser
32
+ mock_user = MagicMock()
33
+ mock_user.is_authenticated = True
34
+ mock_user._user = MagicMock() # The underlying model
35
+ mock_user._user.email = "test@example.com"
36
+
37
+ request = MagicMock()
38
+ request.user = mock_user
39
+
40
+ result = get_request_user(request)
41
+ assert result == mock_user._user
42
+
43
+ def test_returns_user_from_legacy_pattern(self):
44
+ """Test returns user from request.state.user (legacy pattern)."""
45
+ from core.auth.helpers import get_request_user
46
+
47
+ mock_user = MagicMock()
48
+ mock_user.email = "test@example.com"
49
+
50
+ request = MagicMock()
51
+ request.user = MagicMock()
52
+ request.user.is_authenticated = False # Not authenticated via Starlette
53
+ request.state = MagicMock()
54
+ request.state.user = mock_user
55
+
56
+ result = get_request_user(request)
57
+ assert result == mock_user
58
+
59
+ def test_prefers_starlette_over_legacy(self):
60
+ """Test prefers request.user over request.state.user."""
61
+ from core.auth.helpers import get_request_user
62
+
63
+ starlette_user = MagicMock()
64
+ starlette_user.is_authenticated = True
65
+ starlette_user._user = MagicMock()
66
+ starlette_user._user.email = "starlette@example.com"
67
+
68
+ legacy_user = MagicMock()
69
+ legacy_user.email = "legacy@example.com"
70
+
71
+ request = MagicMock()
72
+ request.user = starlette_user
73
+ request.state = MagicMock()
74
+ request.state.user = legacy_user
75
+
76
+ result = get_request_user(request)
77
+ # Should return the Starlette user
78
+ assert result.email == "starlette@example.com"
79
+
80
+ def test_handles_missing_state(self):
81
+ """Test handles request without state attribute."""
82
+ from core.auth.helpers import get_request_user
83
+
84
+ request = MagicMock(spec=[]) # No attributes by default
85
+ request.user = None
86
+
87
+ # This should not raise an error
88
+ result = get_request_user(request)
89
+ assert result is None
90
+
91
+
92
+ class TestIsAuthenticated:
93
+ """Test is_authenticated helper function."""
94
+
95
+ def test_returns_false_when_no_user(self):
96
+ """Test returns False when no user."""
97
+ from core.auth.helpers import is_authenticated
98
+
99
+ request = MagicMock()
100
+ request.user = None
101
+ request.state = MagicMock()
102
+ request.state.user = None
103
+
104
+ assert is_authenticated(request) is False
105
+
106
+ def test_returns_true_for_starlette_user(self):
107
+ """Test returns True for Starlette authenticated user."""
108
+ from core.auth.helpers import is_authenticated
109
+
110
+ mock_user = MagicMock()
111
+ mock_user.is_authenticated = True
112
+
113
+ request = MagicMock()
114
+ request.user = mock_user
115
+
116
+ assert is_authenticated(request) is True
117
+
118
+ def test_returns_true_for_legacy_user(self):
119
+ """Test returns True for legacy user."""
120
+ from core.auth.helpers import is_authenticated
121
+
122
+ mock_user = MagicMock()
123
+
124
+ request = MagicMock()
125
+ request.user = MagicMock()
126
+ request.user.is_authenticated = False
127
+ request.state = MagicMock()
128
+ request.state.user = mock_user
129
+
130
+ assert is_authenticated(request) is True
131
+
132
+
133
+ class TestSetRequestUser:
134
+ """Test set_request_user helper function."""
135
+
136
+ def test_sets_user_on_state(self):
137
+ """Test sets user on request.state."""
138
+ from core.auth.helpers import set_request_user
139
+
140
+ mock_user = MagicMock()
141
+ request = MagicMock()
142
+ request.state = MagicMock()
143
+
144
+ set_request_user(request, mock_user)
145
+
146
+ assert request.state.user == mock_user
147
+
148
+ def test_clears_user_when_none(self):
149
+ """Test clears user when None is passed."""
150
+ from core.auth.helpers import set_request_user
151
+
152
+ request = MagicMock()
153
+ request.state = MagicMock()
154
+ request.state.user = MagicMock() # Previous user
155
+
156
+ set_request_user(request, None)
157
+
158
+ assert request.state.user is None
159
+
160
+
161
+ class TestAuthenticatedUserWrapper:
162
+ """Test AuthenticatedUser wrapper class."""
163
+
164
+ def test_is_authenticated_property(self):
165
+ """Test is_authenticated returns True."""
166
+ from core.auth.middleware import AuthenticatedUser
167
+
168
+ mock_model = MagicMock()
169
+ wrapper = AuthenticatedUser(mock_model)
170
+
171
+ assert wrapper.is_authenticated is True
172
+
173
+ def test_proxies_attributes(self):
174
+ """Test proxies attribute access to underlying model."""
175
+ from core.auth.middleware import AuthenticatedUser
176
+
177
+ mock_model = MagicMock()
178
+ mock_model.email = "test@example.com"
179
+ mock_model.id = 123
180
+
181
+ wrapper = AuthenticatedUser(mock_model)
182
+
183
+ assert wrapper.email == "test@example.com"
184
+ assert wrapper.id == 123
185
+
186
+ def test_display_name(self):
187
+ """Test display_name returns email."""
188
+ from core.auth.middleware import AuthenticatedUser
189
+
190
+ mock_model = MagicMock()
191
+ mock_model.email = "test@example.com"
192
+
193
+ wrapper = AuthenticatedUser(mock_model)
194
+
195
+ assert wrapper.display_name == "test@example.com"
196
+
197
+ def test_identity(self):
198
+ """Test identity returns string id."""
199
+ from core.auth.middleware import AuthenticatedUser
200
+
201
+ mock_model = MagicMock()
202
+ mock_model.id = 123
203
+
204
+ wrapper = AuthenticatedUser(mock_model)
205
+
206
+ assert wrapper.identity == "123"