Coverage for licenzy\core.py: 77%

99 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-17 11:06 -0400

1""" 

2🔑 Licenzy Core - License validation and management 

3 

4This module provides the core functionality for license validation, 

5including the main LicenseManager class and the @licensed decorator. 

6""" 

7 

8import os 

9import hmac 

10import hashlib 

11from datetime import datetime, timedelta 

12from typing import Dict, Any, Optional, Tuple, Callable 

13from pathlib import Path 

14from functools import wraps 

15 

16 

17class LicenseError(Exception): 

18 """🚫 Raised when license validation fails""" 

19 pass 

20 

21 

22class LicenseManager: 

23 """ 

24 🔑 Core license management class 

25  

26 Handles license validation, storage, and checking with a clean, 

27 Pythonic API that's perfect for AI tools and indie projects. 

28 """ 

29 

30 def __init__(self, license_key: Optional[str] = None): 

31 """Initialize license manager with optional license key.""" 

32 self.license_key = license_key or self._find_license_key() 

33 self.license_info: Optional[Dict[str, Any]] = None 

34 self._validated = False 

35 

36 def _find_license_key(self) -> Optional[str]: 

37 """🔍 Find license key from environment or file""" 

38 # 1. Check environment variable 

39 env_key = os.environ.get("LICENZY_LICENSE_KEY") 

40 if env_key: 

41 return env_key.strip() 

42 

43 # 2. Check local project directory  

44 local_license = Path(".licenzy_license") 

45 if local_license.exists(): 

46 return local_license.read_text().strip() 

47 

48 # 3. Check user's home directory 

49 home_license = Path.home() / ".licenzy" / "license.key" 

50 if home_license.exists(): 

51 return home_license.read_text().strip() 

52 

53 return None 

54 

55 def validate_license(self) -> Tuple[bool, str]: 

56 """ 

57 🔐 Validate the license key 

58  

59 Returns: 

60 Tuple of (is_valid: bool, message: str) 

61 """ 

62 if not self.license_key: 

63 return False, "No license key found" 

64 

65 try: 

66 # Parse license key format: user_id:plan:expires_timestamp:signature 

67 parts = self.license_key.split(":") 

68 if len(parts) != 4: 

69 return False, "Invalid license key format" 

70 

71 user_id, plan, expires_timestamp, signature = parts 

72 

73 # Check expiration 

74 expires = datetime.fromtimestamp(int(expires_timestamp)) 

75 if datetime.now() > expires: 

76 return False, f"License expired on {expires.strftime('%Y-%m-%d')}" 

77 

78 # Verify HMAC signature 

79 expected_signature = self._generate_signature(user_id, plan, expires_timestamp) 

80 if not hmac.compare_digest(signature, expected_signature): 

81 return False, "Invalid license signature" 

82 

83 # Store license info 

84 self.license_info = { 

85 "user_id": user_id, 

86 "plan": plan, 

87 "expires": expires, 

88 "days_remaining": (expires - datetime.now()).days 

89 } 

90 

91 self._validated = True 

92 return True, f"✅ License valid until {expires.strftime('%Y-%m-%d')} ({self.license_info['days_remaining']} days remaining)" 

93 

94 except Exception as e: 

95 return False, f"License validation error: {e}" 

96 

97 def _generate_signature(self, user_id: str, plan: str, expires: str) -> str: 

98 """🔒 Generate HMAC signature for license validation""" 

99 # In production, use a proper signing key stored securely 

100 secret_key = "licenzy-signing-key-2025" # Replace with actual key 

101 data = f"{user_id}:{plan}:{expires}" 

102 return hmac.new(secret_key.encode(), data.encode(), hashlib.sha256).hexdigest()[:16] 

103 

104 def check_license(self) -> bool: 

105 """ 

106 ✨ Check if license is valid (main API function) 

107  

108 This is the core function that most users will interact with. 

109 It caches the validation result for performance. 

110 """ 

111 # Development mode bypass 

112 if os.environ.get("LICENZY_DEV_MODE") == "true": 

113 return True 

114 

115 if not self._validated: 

116 valid, message = self.validate_license() 

117 if not valid: 

118 self._show_license_warning(message) 

119 return False 

120 return True 

121 

122 def _show_license_warning(self, message: str): 

123 """⚠️ Show friendly license warning to user""" 

124 print("🔑" + "=" * 59) 

125 print("🔑 LICENZY - LICENSE REQUIRED") 

126 print("🔑" + "=" * 59) 

127 print(f"{message}") 

128 print() 

129 print("💡 To activate your license:") 

130 print(" 1. Set environment: LICENZY_LICENSE_KEY=your-key") 

131 print(" 2. Run: licenzy activate your-license-key") 

132 print(" 3. Save to: ~/.licenzy/license.key") 

133 print() 

134 print("🛒 Get a license at: https://your-licensing-site.com") 

135 print("🔑" + "=" * 59) 

136 

137 def get_license_info(self) -> Optional[Dict[str, Any]]: 

138 """📋 Get detailed license information if valid""" 

139 if self.check_license(): 

140 return self.license_info 

141 return None 

142 

143 

144# Global license manager instance 

145_license_manager: Optional[LicenseManager] = None 

146 

147 

148def get_license_manager() -> LicenseManager: 

149 """🎯 Get the global license manager instance (singleton pattern)""" 

150 global _license_manager 

151 if _license_manager is None: 

152 _license_manager = LicenseManager() 

153 return _license_manager 

154 

155 

156def check_license() -> bool: 

157 """ 

158 ✅ Simple function to check if license is valid 

159  

160 This is the main API function for quick license checks. 

161 Perfect for startup validation or feature gating. 

162  

163 Returns: 

164 bool: True if license is valid, False otherwise 

165 """ 

166 return get_license_manager().check_license() 

167 

168 

169def licensed(func: Optional[Callable] = None, *, message: Optional[str] = None): 

170 """ 

171 🎨 @licensed decorator - The main Licenzy decorator 

172  

173 Use this decorator to protect functions that require a valid license. 

174 Clean, Pythonic, and startup-friendly. 

175  

176 Usage: 

177 @licensed 

178 def premium_feature(): 

179 return "This needs a license!" 

180  

181 @licensed(message="Custom error message") 

182 def another_feature(): 

183 return "Also protected!" 

184  

185 Args: 

186 func: Function to protect (when used without parentheses) 

187 message: Custom error message (optional) 

188 """ 

189 def decorator(f: Callable) -> Callable: 

190 @wraps(f) 

191 def wrapper(*args, **kwargs): 

192 if not check_license(): 

193 error_msg = message or f"🔑 License required to access '{f.__name__}'" 

194 raise LicenseError(error_msg) 

195 return f(*args, **kwargs) 

196 return wrapper 

197 

198 # Handle both @licensed and @licensed() usage 

199 if func is None: 

200 return decorator 

201 else: 

202 return decorator(func) 

203 

204 

205# Friendly aliases for different use cases 

206def access_granted() -> bool: 

207 """🎉 Alias for check_license() - more expressive for some use cases""" 

208 return check_license() 

209 

210 

211def require_key(func: Callable) -> Callable: 

212 """🗝️ Alias for @licensed decorator - alternative naming""" 

213 return licensed(func) 

214 

215 

216def unlock(func: Callable) -> Callable: 

217 """🔓 Alias for @licensed decorator - playful naming""" 

218 return licensed(func)