Coverage for src / lexigram / contracts / auth / models.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-19 05:41 +0800

1"""Authentication data models and protocols. 

2 

3Shared auth data models (pure dataclasses with no external deps) live here 

4so both lexigram-auth and other packages can reference them without 

5creating cross-extension imports. 

6""" 

7 

8from __future__ import annotations 

9 

10from dataclasses import dataclass, field 

11from datetime import UTC, datetime 

12from typing import Any, Protocol, runtime_checkable 

13 

14 

15@runtime_checkable 

16class UserIdentityProtocol(Protocol): 

17 """Protocol for a user's identity.""" 

18 

19 @property 

20 def user_id(self) -> str: ... 

21 

22 @property 

23 def email(self) -> str: ... 

24 

25 

26@dataclass(frozen=True) 

27class UserSession: 

28 """User session data model. 

29 

30 Tracks a specific device/session for a user. Defined in contracts so 

31 both lexigram-auth and other packages can share the type without 

32 creating a cross-extension dependency. 

33 """ 

34 

35 session_id: str 

36 user_id: str 

37 device_id: str 

38 ip_address: str | None = None 

39 user_agent: str | None = None 

40 geo_location: dict[str, Any] = field(default_factory=dict) 

41 fingerprint: dict[str, Any] = field(default_factory=dict) 

42 is_active: bool = True 

43 expires_at: datetime | None = None 

44 last_active_at: datetime | None = None 

45 mfa_verified_at: datetime | None = None 

46 created_at: datetime | None = None 

47 updated_at: datetime | None = None 

48 

49 def is_expired(self) -> bool: 

50 """Return True if the session has passed its expiry timestamp.""" 

51 if not self.expires_at: 

52 return False 

53 now = datetime.now(UTC) if self.expires_at.tzinfo else datetime.now() 

54 return self.expires_at < now 

55 

56 

57@dataclass(frozen=True) 

58class VerifiedIdentityClaims: 

59 """Verified external identity claims returned by OAuth providers. 

60 

61 This value object captures the normalized claims extracted after a 

62 provider-specific verification step. It is intentionally provider-neutral 

63 so apps can hand the result to an identity resolver without touching raw 

64 HTTP responses or provider-specific dict shapes. 

65 """ 

66 

67 provider: str 

68 provider_user_id: str 

69 email: str | None = None 

70 email_verified: bool = False 

71 name: str | None = None 

72 picture: str | None = None 

73 issuer: str | None = None 

74 audience: str | None = None 

75 expires_at: datetime | None = None 

76 issued_at: datetime | None = None 

77 raw_data: dict[str, Any] = field(default_factory=dict) 

78 

79 

80__all__ = ["UserIdentityProtocol", "UserSession", "VerifiedIdentityClaims"]