|
3 | 3 | import uuid |
4 | 4 | from datetime import datetime, timezone |
5 | 5 | from importlib.resources import as_file, files |
6 | | -from typing import Any, Dict, Optional |
| 6 | +from typing import Any, Dict, List, Optional |
7 | 7 |
|
8 | 8 | from access_moppy import _creator |
9 | 9 |
|
10 | 10 |
|
| 11 | +class VariableNotFoundError(ValueError): |
| 12 | + """ |
| 13 | + Exception raised when a requested variable is not found in the specified CMIP6 table. |
| 14 | +
|
| 15 | + Provides helpful suggestions for alternative tables or similar variables. |
| 16 | + """ |
| 17 | + |
| 18 | + def __init__( |
| 19 | + self, |
| 20 | + variable_name: str, |
| 21 | + table_name: str, |
| 22 | + suggestions: Optional[List[str]] = None, |
| 23 | + ): |
| 24 | + self.variable_name = variable_name |
| 25 | + self.table_name = table_name |
| 26 | + self.suggestions = suggestions or [] |
| 27 | + |
| 28 | + message = f"Variable '{variable_name}' not found in CMIP6 table '{table_name}'." |
| 29 | + |
| 30 | + if self.suggestions: |
| 31 | + message += "\n\nSuggestions:\n" + "\n".join( |
| 32 | + f" • {s}" for s in self.suggestions |
| 33 | + ) |
| 34 | + |
| 35 | + super().__init__(message) |
| 36 | + |
| 37 | + |
11 | 38 | class CMIP6Vocabulary: |
12 | 39 | cv_dir = "access_moppy.vocabularies.cmip6_cmor_tables.CMIP6_CVs" |
13 | 40 | table_dir = "access_moppy.vocabularies.cmip6_cmor_tables.Tables" |
@@ -140,9 +167,94 @@ def _get_variable_entry(self) -> Dict[str, Any]: |
140 | 167 |
|
141 | 168 | return var_entry |
142 | 169 | except KeyError: |
143 | | - raise ValueError( |
144 | | - f"Variable '{self.cmor_name}' not found in table {self.table}." |
| 170 | + # Generate helpful suggestions |
| 171 | + suggestions = self._get_variable_suggestions() |
| 172 | + raise VariableNotFoundError(self.cmor_name, self.table, suggestions) |
| 173 | + |
| 174 | + def _get_variable_suggestions(self) -> List[str]: |
| 175 | + """ |
| 176 | + Generate helpful suggestions when a variable is not found. |
| 177 | +
|
| 178 | + Returns: |
| 179 | + List of suggestion strings for the user. |
| 180 | + """ |
| 181 | + suggestions = [] |
| 182 | + |
| 183 | + # Check if variable exists in other CMIP6 tables |
| 184 | + common_tables = ["Amon", "Lmon", "Omon", "Emon", "day", "6hrLev", "3hr"] |
| 185 | + found_in_tables = [] |
| 186 | + |
| 187 | + for table in common_tables: |
| 188 | + if table == self.table: |
| 189 | + continue # Skip current table |
| 190 | + |
| 191 | + try: |
| 192 | + table_file = f"CMIP6_{table}.json" |
| 193 | + table_resource = files(self.table_dir) / table_file |
| 194 | + |
| 195 | + with as_file(table_resource) as table_path: |
| 196 | + with open(table_path, "r", encoding="utf-8") as f: |
| 197 | + table_data = json.load(f) |
| 198 | + |
| 199 | + if self.cmor_name in table_data.get("variable_entry", {}): |
| 200 | + found_in_tables.append(table) |
| 201 | + |
| 202 | + except (FileNotFoundError, KeyError): |
| 203 | + continue # Table doesn't exist or has no variable_entry |
| 204 | + |
| 205 | + if found_in_tables: |
| 206 | + table_list = ", ".join(found_in_tables) |
| 207 | + suggestions.append( |
| 208 | + f"Variable '{self.cmor_name}' is available in table(s): {table_list}" |
145 | 209 | ) |
| 210 | + suggestions.append(f"Try using: {found_in_tables[0]}.{self.cmor_name}") |
| 211 | + |
| 212 | + # Check for similar variable names in current table |
| 213 | + try: |
| 214 | + current_table_data = self._load_table() |
| 215 | + available_vars = list(current_table_data.get("variable_entry", {}).keys()) |
| 216 | + |
| 217 | + # Find variables with similar names (simple string similarity) |
| 218 | + similar_vars = [] |
| 219 | + for var in available_vars: |
| 220 | + if len(var) > 2 and ( |
| 221 | + self.cmor_name.lower() in var.lower() |
| 222 | + or var.lower() in self.cmor_name.lower() |
| 223 | + or |
| 224 | + # Check for common root (first 3 characters) |
| 225 | + ( |
| 226 | + len(self.cmor_name) >= 3 |
| 227 | + and len(var) >= 3 |
| 228 | + and self.cmor_name[:3].lower() == var[:3].lower() |
| 229 | + ) |
| 230 | + ): |
| 231 | + similar_vars.append(var) |
| 232 | + |
| 233 | + if similar_vars: |
| 234 | + similar_list = ", ".join(similar_vars[:5]) # Limit to 5 suggestions |
| 235 | + suggestions.append( |
| 236 | + f"Similar variables in {self.table} table: {similar_list}" |
| 237 | + ) |
| 238 | + |
| 239 | + # Show a sample of available variables if no similar ones found |
| 240 | + elif available_vars: |
| 241 | + sample_vars = ", ".join(available_vars[:10]) # Show first 10 |
| 242 | + total_count = len(available_vars) |
| 243 | + if total_count > 10: |
| 244 | + sample_vars += f" (and {total_count - 10} more)" |
| 245 | + suggestions.append( |
| 246 | + f"Available variables in {self.table} table: {sample_vars}" |
| 247 | + ) |
| 248 | + |
| 249 | + except Exception: |
| 250 | + pass # Don't fail if we can't load suggestions |
| 251 | + |
| 252 | + # Add general guidance |
| 253 | + suggestions.append( |
| 254 | + "Visit https://clipc-services.ceda.ac.uk/dreq/index.html to browse CMIP6 variables" |
| 255 | + ) |
| 256 | + |
| 257 | + return suggestions |
146 | 258 |
|
147 | 259 | def _get_axes(self) -> Dict[str, Any]: |
148 | 260 | # Resolve resource inside the module path |
|
0 commit comments