Coverage for src/alprina_cli/utils/errors.py: 39%

57 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2Friendly error messages for Alprina CLI. 

3 

4All errors are user-friendly with clear solutions. 

5""" 

6 

7from rich.console import Console 

8from rich.panel import Panel 

9from rich import box 

10 

11console = Console() 

12 

13 

14class AlprinaError(Exception): 

15 """Base class for all Alprina CLI errors.""" 

16 

17 def __init__(self, message: str, solution: str = None, title: str = "Error"): 

18 self.message = message 

19 self.solution = solution 

20 self.title = title 

21 super().__init__(message) 

22 

23 def display(self): 

24 """Display error with solution in a nice panel.""" 

25 error_text = f"[bold red]❌ {self.message}[/bold red]" 

26 

27 if self.solution: 

28 error_text += f"\n\n[yellow]💡 Solution:[/yellow]\n{self.solution}" 

29 

30 console.print(Panel.fit( 

31 error_text, 

32 title=self.title, 

33 border_style="red", 

34 box=box.ROUNDED 

35 )) 

36 

37 

38class AuthenticationError(AlprinaError): 

39 """Raised when user is not authenticated.""" 

40 

41 def __init__(self): 

42 super().__init__( 

43 message="You're not signed in", 

44 solution="Run: [bold]alprina auth login[/bold]\n\n" 

45 "Or get your API key from: https://platform.alprina.com/api-keys", 

46 title="Authentication Required" 

47 ) 

48 

49 

50class RateLimitError(AlprinaError): 

51 """Raised when user hits rate limit.""" 

52 

53 def __init__(self, limit: int, reset_time: str = "in 1 hour"): 

54 super().__init__( 

55 message=f"You've reached your scan limit ({limit} scans)", 

56 solution=f"Your limit resets {reset_time}\n\n" 

57 f"Or upgrade for more scans: [bold]https://alprina.com/pricing[/bold]", 

58 title="Rate Limit Reached" 

59 ) 

60 

61 

62class APIError(AlprinaError): 

63 """Raised when API request fails.""" 

64 

65 def __init__(self, status_code: int, message: str = None): 

66 solutions = { 

67 400: "Check your request parameters", 

68 401: "Your API key is invalid or expired\nRun: [bold]alprina auth login[/bold]", 

69 403: "You don't have permission for this action\nUpgrade at: https://alprina.com/pricing", 

70 404: "The requested resource was not found", 

71 429: "Too many requests. Please wait a moment and try again", 

72 500: "Alprina server error. Please try again later\nIf this persists, contact support@alprina.com", 

73 503: "Alprina service is temporarily unavailable\nCheck status: https://status.alprina.com" 

74 } 

75 

76 default_message = message or f"API request failed with status {status_code}" 

77 solution = solutions.get(status_code, "Please try again later\nIf this persists, contact support@alprina.com") 

78 

79 super().__init__( 

80 message=default_message, 

81 solution=solution, 

82 title=f"API Error ({status_code})" 

83 ) 

84 

85 

86class FileNotFoundError(AlprinaError): 

87 """Raised when target file/directory doesn't exist.""" 

88 

89 def __init__(self, path: str): 

90 super().__init__( 

91 message=f"File or directory not found: {path}", 

92 solution="Check the path and try again\n\n" 

93 "Examples:\n" 

94 " [bold]alprina scan ./[/bold] - Scan current directory\n" 

95 " [bold]alprina scan app.py[/bold] - Scan single file\n" 

96 " [bold]alprina scan src/[/bold] - Scan src directory", 

97 title="File Not Found" 

98 ) 

99 

100 

101class NetworkError(AlprinaError): 

102 """Raised when network request fails.""" 

103 

104 def __init__(self, details: str = None): 

105 message = "Cannot connect to Alprina API" 

106 if details: 

107 message += f": {details}" 

108 

109 super().__init__( 

110 message=message, 

111 solution="Check your internet connection\n\n" 

112 "If you're behind a proxy, set these environment variables:\n" 

113 " [bold]HTTP_PROXY=http://proxy:port[/bold]\n" 

114 " [bold]HTTPS_PROXY=https://proxy:port[/bold]\n\n" 

115 "Still having issues? support@alprina.com", 

116 title="Network Error" 

117 ) 

118 

119 

120class InvalidTierError(AlprinaError): 

121 """Raised when user's tier doesn't support a feature.""" 

122 

123 def __init__(self, feature: str, required_tier: str): 

124 super().__init__( 

125 message=f"This feature requires {required_tier} tier", 

126 solution=f"Upgrade to unlock {feature}:\n" 

127 f"[bold]https://alprina.com/pricing[/bold]\n\n" 

128 f"Or contact sales for custom plans:\n" 

129 f"sales@alprina.com", 

130 title="Upgrade Required" 

131 ) 

132 

133 

134class ScanError(AlprinaError): 

135 """Raised when scan fails.""" 

136 

137 def __init__(self, reason: str = None): 

138 message = "Security scan failed" 

139 if reason: 

140 message += f": {reason}" 

141 

142 super().__init__( 

143 message=message, 

144 solution="Try again with verbose mode for more details:\n" 

145 "[bold]alprina scan ./ --verbose[/bold]\n\n" 

146 "Or check logs at:\n" 

147 "~/.alprina/logs/alprina.log", 

148 title="Scan Failed" 

149 ) 

150 

151 

152def handle_error(error: Exception, verbose: bool = False): 

153 """ 

154 Handle any error and display it nicely. 

155  

156 Args: 

157 error: The exception to handle 

158 verbose: Show full traceback 

159 """ 

160 if isinstance(error, AlprinaError): 

161 error.display() 

162 elif isinstance(error, KeyboardInterrupt): 

163 console.print("\n[yellow]⚠️ Operation cancelled by user[/yellow]") 

164 elif isinstance(error, FileNotFoundError): 

165 FileNotFoundError(str(error)).display() 

166 else: 

167 # Generic error 

168 console.print(Panel.fit( 

169 f"[bold red]❌ Unexpected error:[/bold red]\n\n" 

170 f"{str(error)}\n\n" 

171 f"[yellow]💡 What to do:[/yellow]\n" 

172 f"1. Try running with [bold]--verbose[/bold] for more details\n" 

173 f"2. Check logs at [bold]~/.alprina/logs/alprina.log[/bold]\n" 

174 f"3. Report this bug: [bold]https://github.com/alprina/issues[/bold]", 

175 title="Error", 

176 border_style="red", 

177 box=box.ROUNDED 

178 )) 

179 

180 if verbose: 

181 import traceback 

182 console.print("\n[dim]Full traceback:[/dim]") 

183 console.print(traceback.format_exc())