Coverage for licenzy\core.py: 77%

1"""

2🔑 Licenzy Core - License validation and management

3

4 This module provides the core functionality for license validation,

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

6"""

7

8 import os

9 import hmac

10 import hashlib

11 from datetime import datetime, timedelta

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

13 from pathlib import Path

14 from functools import wraps

15

16

17 class LicenseError(Exception):

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

19 pass

20

21

22 class 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

148 def 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

156 def 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

169 def 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

206 def access_granted() -> bool:

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

208 return check_license()

209

210

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

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

213 return licensed(func)

214

215

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

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

218 return licensed(func)