Coverage for src/lexigram/auth/exceptions.py: 86%
104 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 00:58 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 00:58 +0800
1"""Exception hierarchy for Lexigram Auth.
3All exceptions are organized by inheritance level:
41. Re-imports from lexigram-contracts (base classes, aliased into this hierarchy)
52. Auth root exception
63. Authentication exceptions (credentials, tokens, account)
74. Authorization exceptions
85. Verification exceptions
96. Registration/conflict exceptions
10"""
12from __future__ import annotations
14from typing import Any
16from lexigram.contracts.auth.exceptions import AuthError as ContractsAuthError
17from lexigram.contracts.auth.exceptions import VerificationError
18from lexigram.contracts.exceptions import (
19 AuthenticationError as LexigramAuthenticationError,
20)
21from lexigram.contracts.exceptions import (
22 AuthorizationError as LexigramAuthorizationError,
23)
24from lexigram.contracts.exceptions import (
25 ConflictError,
26)
27from lexigram.contracts.exceptions import (
28 NotFoundError as LexigramNotFoundError,
29)
32class AuthError(ContractsAuthError):
33 """Base exception for all auth errors."""
35 _code = "LEX_ERR_AUTH_004"
38class AuthenticationError(LexigramAuthenticationError, AuthError):
39 """Raised when authentication fails."""
41 _code = "LEX_ERR_AUTH_005"
44class AuthorizationError(LexigramAuthorizationError, AuthError):
45 """Raised when user lacks required permissions."""
47 _code = "LEX_ERR_AUTH_006"
50class InvalidCredentialsError(AuthenticationError):
51 """Raised when credentials are invalid."""
53 _code = "LEX_ERR_AUTH_007"
55 def __init__(self, message: str = "Invalid credentials", **kwargs: Any):
56 super().__init__(message, **kwargs)
59class AccountLockedError(AuthenticationError):
60 """Raised when an account is locked due to too many failed login attempts.
62 Accounts are locked automatically after ``LockoutConfig.max_failed_attempts``
63 consecutive failures within ``LockoutConfig.lockout_duration_seconds``.
64 The lock is lifted automatically once the observation window has passed.
65 """
67 _code = "LEX_ERR_AUTH_008"
69 def __init__(self, email: str = "", **kwargs: Any):
70 msg = (
71 f"Account locked due to too many failed login attempts: {email}"
72 if email
73 else "Account locked due to too many failed login attempts"
74 )
75 super().__init__(msg, **kwargs)
78class UserNotFoundError(LexigramNotFoundError, AuthError):
79 """Raised when user is not found."""
81 _code = "LEX_ERR_AUTH_009"
83 def __init__(self, identifier: str, **kwargs: Any):
84 super().__init__(
85 f"User not found: {identifier}",
86 **kwargs,
87 )
90class TokenError(InvalidCredentialsError):
91 """Base exception for token-related errors."""
93 _code = "LEX_ERR_AUTH_010"
96class InvalidTokenError(TokenError):
97 """Raised when a token is malformed or invalid."""
99 _code = "LEX_ERR_AUTH_011"
102class TokenExpiredError(TokenError):
103 """Raised when a token has expired."""
105 _code = "LEX_ERR_AUTH_012"
107 def __init__(
108 self,
109 message: str = "Token has expired",
110 expiration_time: str | None = None,
111 **kwargs: Any,
112 ) -> None:
113 details = kwargs.get("details", {})
114 if expiration_time:
115 details["expiration_time"] = expiration_time
116 kwargs["details"] = details
117 super().__init__(message, **kwargs)
120class TokenBlacklistedError(TokenError):
121 """Token has been explicitly revoked."""
123 _code = "LEX_ERR_AUTH_013"
125 def __init__(
126 self,
127 message: str = "Token has been revoked",
128 **kwargs: Any,
129 ) -> None:
130 super().__init__(message, **kwargs)
133class TokenInvalidError(TokenError):
134 """Token is structurally invalid or has wrong type."""
136 _code = "LEX_ERR_AUTH_014"
138 def __init__(
139 self,
140 message: str = "Token is invalid",
141 reason: str | None = None,
142 **kwargs: Any,
143 ) -> None:
144 details = kwargs.get("details", {})
145 if reason:
146 details["reason"] = reason
147 kwargs["details"] = details
148 super().__init__(message, **kwargs)
151class TokenAudienceError(TokenError):
152 """Token audience claim does not match expected."""
154 _code = "LEX_ERR_AUTH_015"
156 def __init__(
157 self,
158 message: str = "Token audience mismatch",
159 expected: str | None = None,
160 actual: str | None = None,
161 **kwargs: Any,
162 ) -> None:
163 details = kwargs.get("details", {})
164 if expected:
165 details["expected"] = expected
166 if actual:
167 details["actual"] = actual
168 kwargs["details"] = details
169 super().__init__(message, **kwargs)
172class TokenNotFoundError(TokenError):
173 """Token record does not exist."""
175 _code = "LEX_ERR_AUTH_016"
177 def __init__(
178 self,
179 message: str = "Token not found",
180 token_id: str | None = None,
181 **kwargs: Any,
182 ) -> None:
183 details = kwargs.get("details", {})
184 if token_id:
185 details["token_id"] = token_id
186 kwargs["details"] = details
187 super().__init__(message, **kwargs)
190class InvalidAudienceError(TokenError):
191 """Raised when a token audience is invalid."""
193 _code = "LEX_ERR_AUTH_017"
196class InvalidScopeError(TokenError):
197 """Raised when a token lacks required scope."""
199 _code = "LEX_ERR_AUTH_018"
202class BlacklistedTokenError(TokenError):
203 """Raised when a token has been blacklisted."""
205 _code = "LEX_ERR_AUTH_019"
208class TokenExpiredVerificationError(VerificationError):
209 """Account verification has expired."""
211 _code = "LEX_ERR_AUTH_020"
213 def __init__(
214 self,
215 message: str = "Verification has expired",
216 user_id: str | None = None,
217 **kwargs: Any,
218 ) -> None:
219 details = kwargs.get("details", {})
220 if user_id:
221 details["user_id"] = user_id
222 kwargs["details"] = details
223 super().__init__(message, **kwargs)
226class AlreadyVerifiedError(VerificationError):
227 """Account is already verified."""
229 _code = "LEX_ERR_AUTH_021"
231 def __init__(
232 self,
233 message: str = "Account is already verified",
234 user_id: str | None = None,
235 **kwargs: Any,
236 ) -> None:
237 details = kwargs.get("details", {})
238 if user_id:
239 details["user_id"] = user_id
240 kwargs["details"] = details
241 super().__init__(message, **kwargs)
244class EmailExistsError(AuthError, ConflictError):
245 """Raised when email is already taken."""
247 _code = "LEX_ERR_AUTH_022"
250class UsernameExistsError(AuthError, ConflictError):
251 """Raised when username is already taken."""
253 _code = "LEX_ERR_AUTH_023"
256class PasswordPolicyError(AuthError):
257 """Raised when password doesn't meet requirements."""
259 _code = "LEX_ERR_AUTH_024"
262class OAuth2Error(AuthError):
263 """Base exception for OAuth2 errors."""
265 _code = "LEX_ERR_AUTH_025"
268class SessionNotFoundError(LexigramNotFoundError, AuthError):
269 """Raised when a session cannot be found in the store."""
271 _code = "LEX_ERR_AUTH_026"
273 def __init__(self, session_id: str, **kwargs: Any):
274 super().__init__(
275 f"Session not found: {session_id}",
276 **kwargs,
277 )
280__all__ = [
281 "AccountLockedError",
282 "AlreadyVerifiedError",
283 "AuthError",
284 "AuthenticationError",
285 "AuthorizationError",
286 "BlacklistedTokenError",
287 "EmailExistsError",
288 "InvalidAudienceError",
289 "InvalidCredentialsError",
290 "InvalidScopeError",
291 "InvalidTokenError",
292 "OAuth2Error",
293 "PasswordPolicyError",
294 "SessionNotFoundError",
295 "TokenAudienceError",
296 "TokenBlacklistedError",
297 "TokenError",
298 "TokenExpiredError",
299 "TokenExpiredVerificationError",
300 "TokenInvalidError",
301 "TokenNotFoundError",
302 "UserNotFoundError",
303 "UsernameExistsError",
304 "VerificationError",
305]