215 lines
7.2 KiB
Python
215 lines
7.2 KiB
Python
"""
|
|
base.py
|
|
Description: Base class for all metadata handlers with common functionality
|
|
This module provides a base class for handling metadata operations, including reading and writing
|
|
metadata, logging, and error handling. It is designed to be extended by specific metadata handler classes.
|
|
Author: Eric Hiss (GitHub: EricRollei)
|
|
Contact: [eric@historic.camera, eric@rollei.us]
|
|
Version: 1.0.0
|
|
Date: [March 2025]
|
|
License: Dual License (Non-Commercial and Commercial Use)
|
|
Copyright (c) 2025 Eric Hiss. All rights reserved.
|
|
|
|
Dual License:
|
|
1. Non-Commercial Use: This software is licensed under the terms of the
|
|
Creative Commons Attribution-NonCommercial 4.0 International License.
|
|
To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
|
|
|
|
2. Commercial Use: For commercial use, a separate license is required.
|
|
Please contact Eric Hiss at [eric@historic.camera, eric@rollei.us] for licensing options.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
|
|
Dependencies:
|
|
|
|
|
|
"""
|
|
# Metadata_system/src/eric_metadata/handlers/base.py
|
|
import os
|
|
import threading
|
|
import datetime
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
class BaseHandler:
|
|
"""Base class for all metadata handlers with common functionality"""
|
|
|
|
def __init__(self, debug: bool = False):
|
|
"""
|
|
Initialize the base handler
|
|
|
|
Args:
|
|
debug: Whether to enable debug logging
|
|
"""
|
|
self.debug = debug
|
|
self._lock = threading.Lock()
|
|
self.error_history = []
|
|
self.max_error_history = 100
|
|
self._operation_status = {
|
|
'in_progress': False,
|
|
'operation': None,
|
|
'retries': 0,
|
|
'recovery_attempted': False
|
|
}
|
|
|
|
def log(self, message: str, level: str = "INFO", error: Exception = None) -> None:
|
|
"""
|
|
Log a message with appropriate level
|
|
|
|
Args:
|
|
message: The message to log
|
|
level: Log level (INFO, DEBUG, WARNING, ERROR)
|
|
error: Optional exception to include in log
|
|
"""
|
|
if level == "DEBUG" and not self.debug:
|
|
return
|
|
|
|
timestamp = self.get_timestamp()
|
|
|
|
error_text = f" - {str(error)}" if error else ""
|
|
log_message = f"[{timestamp}] {self.__class__.__name__} [{level}] {message}{error_text}"
|
|
|
|
print(log_message)
|
|
|
|
# Track errors for recovery
|
|
if level in ["ERROR", "WARNING"]:
|
|
self.error_history.append({
|
|
'timestamp': timestamp,
|
|
'level': level,
|
|
'message': message,
|
|
'error': str(error) if error else None
|
|
})
|
|
|
|
# Maintain history size
|
|
if len(self.error_history) > self.max_error_history:
|
|
self.error_history.pop(0)
|
|
|
|
def get_timestamp(self) -> str:
|
|
"""Get current timestamp in ISO 8601 format"""
|
|
return datetime.datetime.now().isoformat()
|
|
|
|
def write_metadata(self, filepath: str, metadata: Dict[str, Any]) -> bool:
|
|
"""
|
|
Abstract method to write metadata to a file
|
|
|
|
Args:
|
|
filepath: Path to the file
|
|
metadata: Metadata to write
|
|
|
|
Returns:
|
|
bool: Success status
|
|
"""
|
|
raise NotImplementedError("Subclasses must implement write_metadata")
|
|
|
|
def read_metadata(self, filepath: str) -> Dict[str, Any]:
|
|
"""
|
|
Abstract method to read metadata from a file
|
|
|
|
Args:
|
|
filepath: Path to the file
|
|
|
|
Returns:
|
|
dict: Metadata read from the file
|
|
"""
|
|
raise NotImplementedError("Subclasses must implement read_metadata")
|
|
|
|
def _safely_execute(self, operation_name: str, callback, *args, **kwargs) -> Any:
|
|
"""
|
|
Execute operation with proper locking and error handling
|
|
|
|
Args:
|
|
operation_name: Name of the operation for logging
|
|
callback: Function to execute
|
|
*args, **kwargs: Arguments to pass to the callback
|
|
|
|
Returns:
|
|
Any: Result from the callback or None on error
|
|
"""
|
|
with self._lock:
|
|
try:
|
|
# Track operation
|
|
self._operation_status['in_progress'] = True
|
|
self._operation_status['operation'] = operation_name
|
|
self._operation_status['recovery_attempted'] = False
|
|
|
|
self.log(f"Starting operation: {operation_name}", level="DEBUG")
|
|
result = callback(*args, **kwargs)
|
|
self.log(f"Completed operation: {operation_name}", level="DEBUG")
|
|
|
|
# Reset operation status
|
|
self._operation_status['in_progress'] = False
|
|
self._operation_status['operation'] = None
|
|
self._operation_status['retries'] = 0
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.log(f"Error in {operation_name}: {str(e)}", level="ERROR", error=e)
|
|
|
|
# Track error for recovery
|
|
self._operation_status['in_progress'] = False
|
|
|
|
return None
|
|
|
|
def _get_sidecar_path(self, filepath: str) -> str:
|
|
"""
|
|
Get the path to the XMP sidecar file
|
|
|
|
Args:
|
|
filepath: Path to the original file
|
|
|
|
Returns:
|
|
str: Path to the XMP sidecar file
|
|
"""
|
|
# Get base path without extension
|
|
base_path, _ = os.path.splitext(filepath)
|
|
|
|
# Return path with .xmp extension
|
|
sidecar_path = f"{base_path}.xmp"
|
|
|
|
# Debug log
|
|
if self.debug:
|
|
self.log(f"Input filepath: {filepath}", level="DEBUG")
|
|
self.log(f"Generated sidecar path: {sidecar_path}", level="DEBUG")
|
|
self.log(f"Path exists: {os.path.exists(os.path.dirname(sidecar_path))}", level="DEBUG")
|
|
|
|
return sidecar_path
|
|
|
|
def _get_text_file_path(self, filepath: str) -> str:
|
|
"""
|
|
Get the path to the text metadata file
|
|
|
|
Args:
|
|
filepath: Path to the original file
|
|
|
|
Returns:
|
|
str: Path to the text metadata file
|
|
"""
|
|
# Get base path without extension
|
|
base_path, _ = os.path.splitext(filepath)
|
|
|
|
# Return path with .txt extension
|
|
return f"{base_path}.txt"
|
|
|
|
def set_resource_identifier(self, about_uri: str) -> None:
|
|
"""
|
|
Set the resource identifier for XMP metadata
|
|
|
|
Args:
|
|
about_uri: The resource URI to use
|
|
"""
|
|
self.resource_about = about_uri
|
|
|
|
def cleanup(self) -> None:
|
|
"""Clean up any resources used by the handler"""
|
|
# Base implementation does nothing
|
|
pass
|
|
|
|
def __enter__(self):
|
|
"""Context manager entry"""
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Context manager exit - ensure cleanup"""
|
|
self.cleanup() |