Coverage for licenzy\core.py: 77%
99 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-17 11:06 -0400
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-17 11:06 -0400
1"""
2🔑 Licenzy Core - License validation and management
4This module provides the core functionality for license validation,
5including the main LicenseManager class and the @licensed decorator.
6"""
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
17class LicenseError(Exception):
18 """🚫 Raised when license validation fails"""
19 pass
22class LicenseManager:
23 """
24 🔑 Core license management class
26 Handles license validation, storage, and checking with a clean,
27 Pythonic API that's perfect for AI tools and indie projects.
28 """
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
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()
43 # 2. Check local project directory
44 local_license = Path(".licenzy_license")
45 if local_license.exists():
46 return local_license.read_text().strip()
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()
53 return None
55 def validate_license(self) -> Tuple[bool, str]:
56 """
57 🔐 Validate the license key
59 Returns:
60 Tuple of (is_valid: bool, message: str)
61 """
62 if not self.license_key:
63 return False, "No license key found"
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"
71 user_id, plan, expires_timestamp, signature = parts
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')}"
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"
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 }
91 self._validated = True
92 return True, f"✅ License valid until {expires.strftime('%Y-%m-%d')} ({self.license_info['days_remaining']} days remaining)"
94 except Exception as e:
95 return False, f"License validation error: {e}"
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]
104 def check_license(self) -> bool:
105 """
106 ✨ Check if license is valid (main API function)
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
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
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)
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
144# Global license manager instance
145_license_manager: Optional[LicenseManager] = None
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
156def check_license() -> bool:
157 """
158 ✅ Simple function to check if license is valid
160 This is the main API function for quick license checks.
161 Perfect for startup validation or feature gating.
163 Returns:
164 bool: True if license is valid, False otherwise
165 """
166 return get_license_manager().check_license()
169def licensed(func: Optional[Callable] = None, *, message: Optional[str] = None):
170 """
171 🎨 @licensed decorator - The main Licenzy decorator
173 Use this decorator to protect functions that require a valid license.
174 Clean, Pythonic, and startup-friendly.
176 Usage:
177 @licensed
178 def premium_feature():
179 return "This needs a license!"
181 @licensed(message="Custom error message")
182 def another_feature():
183 return "Also protected!"
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
198 # Handle both @licensed and @licensed() usage
199 if func is None:
200 return decorator
201 else:
202 return decorator(func)
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()
211def require_key(func: Callable) -> Callable:
212 """🗝️ Alias for @licensed decorator - alternative naming"""
213 return licensed(func)
216def unlock(func: Callable) -> Callable:
217 """🔓 Alias for @licensed decorator - playful naming"""
218 return licensed(func)