Coverage for src/chuck_data/commands/wizard/validator.py: 0%

85 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-06-05 22:56 -0700

1""" 

2Input validation for setup wizard steps. 

3""" 

4 

5from dataclasses import dataclass 

6from typing import Optional, List, Dict, Any 

7import logging 

8 

9from ...databricks.url_utils import ( 

10 validate_workspace_url, 

11 normalize_workspace_url, 

12 detect_cloud_provider, 

13 get_full_workspace_url, 

14) 

15 

16 

17@dataclass 

18class ValidationResult: 

19 """Result of input validation.""" 

20 

21 is_valid: bool 

22 message: str 

23 processed_value: Optional[str] = None 

24 error_details: Optional[str] = None 

25 

26 

27class InputValidator: 

28 """Handles validation of user inputs for wizard steps.""" 

29 

30 def validate_workspace_url(self, url_input: str) -> ValidationResult: 

31 """Validate and process workspace URL input.""" 

32 if not url_input or not url_input.strip(): 

33 return ValidationResult( 

34 is_valid=False, message="Workspace URL cannot be empty" 

35 ) 

36 

37 url_input = url_input.strip() 

38 

39 try: 

40 # First validate the raw input before processing 

41 is_raw_valid, raw_error = validate_workspace_url(url_input) 

42 

43 if not is_raw_valid: 

44 return ValidationResult( 

45 is_valid=False, 

46 message=raw_error or "Invalid workspace URL format", 

47 ) 

48 

49 # If raw input is valid, process it 

50 normalized_id = normalize_workspace_url(url_input) 

51 cloud_provider = detect_cloud_provider(url_input) 

52 full_url = get_full_workspace_url(normalized_id, cloud_provider) 

53 

54 return ValidationResult( 

55 is_valid=True, 

56 message="Workspace URL validated successfully", 

57 processed_value=full_url, 

58 ) 

59 

60 except Exception as e: 

61 logging.error(f"Error processing workspace URL: {e}") 

62 return ValidationResult( 

63 is_valid=False, 

64 message="Error processing workspace URL", 

65 error_details=str(e), 

66 ) 

67 

68 def validate_token(self, token: str, workspace_url: str) -> ValidationResult: 

69 """Validate Databricks token.""" 

70 if not token or not token.strip(): 

71 return ValidationResult(is_valid=False, message="Token cannot be empty") 

72 

73 token = token.strip() 

74 

75 try: 

76 # Validate token with Databricks API using the provided workspace URL 

77 from ...clients.databricks import DatabricksAPIClient 

78 

79 client = DatabricksAPIClient(workspace_url, token) 

80 is_valid = client.validate_token() 

81 

82 if not is_valid: 

83 return ValidationResult( 

84 is_valid=False, 

85 message="Invalid Databricks token - please check and try again", 

86 ) 

87 

88 return ValidationResult( 

89 is_valid=True, 

90 message="Token validated successfully", 

91 processed_value=token, 

92 ) 

93 

94 except Exception as e: 

95 logging.error(f"Error validating token: {e}") 

96 return ValidationResult( 

97 is_valid=False, message="Error validating token", error_details=str(e) 

98 ) 

99 

100 def validate_model_selection( 

101 self, model_input: str, models: List[Dict[str, Any]] 

102 ) -> ValidationResult: 

103 """Validate model selection input.""" 

104 if not model_input or not model_input.strip(): 

105 return ValidationResult( 

106 is_valid=False, 

107 message="Please select a model by entering its number or name", 

108 ) 

109 

110 if not models: 

111 return ValidationResult( 

112 is_valid=False, message="No models available for selection" 

113 ) 

114 

115 model_input = model_input.strip() 

116 

117 # Try to interpret as an index first 

118 if model_input.isdigit(): 

119 index = int(model_input) - 1 # Convert to 0-based index 

120 if 0 <= index < len(models): 

121 selected_model = models[index]["name"] 

122 return ValidationResult( 

123 is_valid=True, 

124 message=f"Model '{selected_model}' selected", 

125 processed_value=selected_model, 

126 ) 

127 else: 

128 return ValidationResult( 

129 is_valid=False, 

130 message=f"Invalid model number. Please enter a number between 1 and {len(models)}", 

131 ) 

132 

133 # Try to find by exact name (case-insensitive) 

134 for model in models: 

135 if model_input.lower() == model["name"].lower(): 

136 return ValidationResult( 

137 is_valid=True, 

138 message=f"Model '{model['name']}' selected", 

139 processed_value=model["name"], 

140 ) 

141 

142 # Try substring match 

143 matches = [] 

144 for model in models: 

145 if model_input.lower() in model["name"].lower(): 

146 matches.append(model["name"]) 

147 

148 if len(matches) == 1: 

149 return ValidationResult( 

150 is_valid=True, 

151 message=f"Model '{matches[0]}' selected", 

152 processed_value=matches[0], 

153 ) 

154 elif len(matches) > 1: 

155 return ValidationResult( 

156 is_valid=False, 

157 message=f"Multiple models match '{model_input}'. Please be more specific or use a number", 

158 error_details=f"Matching models: {', '.join(matches)}", 

159 ) 

160 

161 return ValidationResult( 

162 is_valid=False, 

163 message=f"Model '{model_input}' not found. Please enter a valid model number or name", 

164 ) 

165 

166 def validate_usage_consent(self, response: str) -> ValidationResult: 

167 """Validate usage consent response.""" 

168 if not response or not response.strip(): 

169 return ValidationResult( 

170 is_valid=False, message="Please enter 'yes' or 'no'" 

171 ) 

172 

173 response = response.strip().lower() 

174 

175 if response in ["yes", "y"]: 

176 return ValidationResult( 

177 is_valid=True, 

178 message="Usage tracking consent granted", 

179 processed_value="yes", 

180 ) 

181 elif response in ["no", "n"]: 

182 return ValidationResult( 

183 is_valid=True, 

184 message="Usage tracking consent declined", 

185 processed_value="no", 

186 ) 

187 else: 

188 return ValidationResult( 

189 is_valid=False, message="Please enter 'yes' or 'no'" 

190 ) 

191 

192 def detect_input_type(self, input_text: str, current_step) -> str: 

193 """Detect what type of input this is based on content and current step.""" 

194 if not input_text or not input_text.strip(): 

195 return "empty" 

196 

197 input_text = input_text.strip() 

198 

199 # For workspace URL step, detect URL-like input 

200 if ( 

201 hasattr(current_step, "WORKSPACE_URL") 

202 and current_step == current_step.WORKSPACE_URL 

203 ): 

204 has_dots = "." in input_text 

205 has_databricks = "databricks" in input_text.lower() 

206 has_protocol = input_text.lower().startswith("http") 

207 

208 if has_dots or has_databricks or has_protocol: 

209 return "url" 

210 else: 

211 return "token" # Assume non-URL input is token 

212 

213 return "text"