Improved file format error handling
This commit is contained in:
parent
ed517f4568
commit
5c3be8803a
1 changed files with 67 additions and 72 deletions
|
|
@ -4,77 +4,67 @@ import json
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
# import shutil
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def write_json_to_file(data: dict, filepath: str) -> bool:
|
def write_json_to_file(data: dict, filepath: str) -> bool:
|
||||||
|
temp_path = None
|
||||||
try:
|
try:
|
||||||
# Create directory if it doesn't exist
|
|
||||||
directory = os.path.dirname(filepath)
|
directory = os.path.dirname(filepath)
|
||||||
if directory and not os.path.exists(directory):
|
if directory and not os.path.exists(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
|
|
||||||
# Write to a temporary file first
|
fd, temp_path = tempfile.mkstemp(dir=directory, suffix='.tmp')
|
||||||
with tempfile.NamedTemporaryFile(mode='w', dir=directory, delete=False, suffix='.json') as f:
|
|
||||||
json.dump(data, f, indent=4)
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
# Only replace the original if successful
|
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||||
shutil.move(temp_path, filepath)
|
# Use 'default' to handle Enums and other non-serializable objects recursively
|
||||||
|
json.dump(data, f, default=lambda o: o.value if isinstance(o, Enum) else str(o))
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
|
os.replace(temp_path, filepath)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except TypeError as e:
|
except (TypeError, ValueError) as e:
|
||||||
logger.error(f"Data is not JSON serializable: {e}")
|
logger.error(f"JSON serialization error: {e}")
|
||||||
|
|
||||||
# Attempt to sanitize Enums by extracting their .value
|
|
||||||
sanitized_data = {k: (v.value if isinstance(v, Enum) else v) for k, v in data.items()}
|
|
||||||
|
|
||||||
# Only retry if we actually modified the data to prevent infinite recursion
|
|
||||||
if sanitized_data != data:
|
|
||||||
return write_json_to_file(sanitized_data, filepath)
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
except FileNotFoundError:
|
except (FileNotFoundError, IsADirectoryError, PermissionError) as e:
|
||||||
logger.error(f"File not found: {filepath}")
|
logger.error(f"File system error for {filepath}: {e}")
|
||||||
return False
|
|
||||||
except IsADirectoryError:
|
|
||||||
logger.error(f"Path is a directory, not a file: {filepath}")
|
|
||||||
return False
|
return False
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(f"Error reading file {filepath}: {e}")
|
logger.error(f"IO Error writing file {filepath}: {e}")
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
# Only attempt to remove if temp_path was created and still exists
|
||||||
def read_entire_json(filepath: str) -> dict:
|
if temp_path and os.path.exists(temp_path):
|
||||||
try:
|
try:
|
||||||
# Check if file exists
|
os.remove(temp_path)
|
||||||
if not os.path.exists(filepath):
|
except OSError:
|
||||||
raise FileNotFoundError(f"File not found: {filepath}")
|
pass
|
||||||
|
|
||||||
# Check if path is a directory
|
|
||||||
if os.path.isdir(filepath):
|
|
||||||
raise IsADirectoryError(f"Path is a directory, not a file: {filepath}")
|
|
||||||
|
|
||||||
# Open and parse JSON file
|
def read_entire_json(filepath: str) -> Optional[dict]:
|
||||||
with open(filepath, "r") as f:
|
"""
|
||||||
data = json.load(f)
|
Reads a JSON file and returns the data.
|
||||||
|
Returns None if the file cannot be read or is missing.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Explicit encoding prevents UnicodeDecodeError.
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise json.JSONDecodeError(
|
|
||||||
f"Invalid JSON in file {filepath}: {e.msg}", e.doc, e.pos
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"File not found: {filepath}")
|
logger.error(f"File not found: {filepath}")
|
||||||
return False
|
|
||||||
except IsADirectoryError:
|
except IsADirectoryError:
|
||||||
logger.error(f"Path is a directory, not a file: {filepath}")
|
logger.error(f"Path is a directory, not a file: {filepath}")
|
||||||
return False
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Invalid JSON in file {filepath}: {e.msg}")
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(f"Error reading file {filepath}: {e}")
|
logger.error(f"IO Error reading file {filepath}: {e}")
|
||||||
return False
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _search_recursive(obj: Any, search_key: str) -> Any | None:
|
def _search_recursive(obj: Any, search_key: str) -> Any | None:
|
||||||
|
|
@ -98,48 +88,53 @@ def _search_recursive(obj: Any, search_key: str) -> Any | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_value_from_json_file(filepath: str, key: str, category: str | None = None) -> Any:
|
|
||||||
|
def get_value_from_json_file(filepath: str, key: str, category: str | None = None) -> Any | None:
|
||||||
"""
|
"""
|
||||||
Load JSON file and search for a key recursively.
|
Load JSON file and search for a key recursively.
|
||||||
|
Returns the value if found and valid; otherwise returns None.
|
||||||
Args:
|
|
||||||
filepath: Path to the JSON file
|
|
||||||
key: The key to search for
|
|
||||||
category: (Optional) Top-level key to limit search scope. If None, searches entire document.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The value associated with the key
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If value is None, empty string, or empty collection
|
|
||||||
KeyError: If category or key is not found
|
|
||||||
"""
|
"""
|
||||||
data = read_entire_json(filepath)
|
data = read_entire_json(filepath)
|
||||||
|
|
||||||
|
# System/Read error
|
||||||
|
if data is None:
|
||||||
|
# Note: read_entire_json already logged the specific reason (FileNotFound, etc.)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Empty file
|
||||||
|
if not data:
|
||||||
|
logger.error(f"JSON file is empty: {filepath}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle category scope
|
||||||
if category:
|
if category:
|
||||||
if not isinstance(category, str):
|
if not isinstance(category, str):
|
||||||
error_msg = f"Error in get_value_from_json_file, category must be a string, not {type(category)}: {category}"
|
logger.error(f"Category must be a string, not {type(category)}: {category}")
|
||||||
logger.error(error_msg)
|
return None
|
||||||
raise ValueError(error_msg)
|
|
||||||
|
|
||||||
if category not in data:
|
if category not in data:
|
||||||
raise KeyError(f"Category '{category}' not found in JSON file")
|
logger.error(f"Category '{category}' not found in {filepath}")
|
||||||
|
return None
|
||||||
subcategory = data[category]
|
subcategory = data[category]
|
||||||
else:
|
else:
|
||||||
subcategory = data
|
subcategory = data
|
||||||
|
|
||||||
|
# Search for the key
|
||||||
value = _search_recursive(subcategory, key)
|
value = _search_recursive(subcategory, key)
|
||||||
|
|
||||||
|
# Guard: Key not found
|
||||||
if value is None:
|
if value is None:
|
||||||
raise KeyError(f"Key '{key}' not found" + (f" in category '{category}'" if category else ""))
|
logger.error(f"Key '{key}' not found" + (f" in category '{category}'" if category else ""))
|
||||||
|
return None
|
||||||
|
|
||||||
# Validation
|
# Guard: Validation (Empty strings or collections)
|
||||||
if isinstance(value, str):
|
if isinstance(value, str) and value.strip() == "":
|
||||||
if value.strip() == "":
|
logger.error(f"Value for key '{key}' is empty or whitespace-only")
|
||||||
raise ValueError(f"Value for key '{key}' is empty or whitespace-only")
|
return None
|
||||||
elif isinstance(value, (list, dict)):
|
|
||||||
if len(value) == 0:
|
if isinstance(value, (list, dict)) and len(value) == 0:
|
||||||
raise ValueError(f"Value for key '{key}' is an empty collection")
|
logger.error(f"Value for key '{key}' is an empty collection")
|
||||||
|
return None
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue