SXXXXXXX_FlightMonitor/flightmonitor/controller/aircraft_db_importer.py
2025-06-13 11:48:49 +02:00

314 lines
13 KiB
Python

# FlightMonitor/controller/aircraft_db_importer.py
"""
Handles the import of aircraft database from CSV files,
including progress tracking and multi-threading for the import process.
"""
import os
import csv
import threading
import time
from typing import Optional, Any, Callable, Tuple, TYPE_CHECKING
from flightmonitor.utils.logger import get_logger
# Type checking imports to avoid circular dependencies at runtime
if TYPE_CHECKING:
from flightmonitor.data.aircraft_database_manager import AircraftDatabaseManager
from flightmonitor.gui.main_window import MainWindow
from flightmonitor.gui.dialogs.import_progress_dialog import ImportProgressDialog
module_logger = get_logger(__name__)
# Constants for CSV processing
CSV_FIELD_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 # 10 MB
PROGRESS_CALLBACK_INTERVAL_ROWS = 500 # How often to update the progress bar
class AircraftDBImporter:
"""
Manages the process of importing aircraft data from a CSV file into the database.
Designed to run the import in a separate thread and report progress to a GUI dialog.
"""
def __init__(
self,
aircraft_db_manager: "AircraftDatabaseManager",
main_window: "MainWindow",
):
"""
Initializes the AircraftDBImporter.
Args:
aircraft_db_manager: An instance of AircraftDatabaseManager to perform DB operations.
main_window: A reference to the main application window (for GUI updates and messages).
"""
self.aircraft_db_manager = aircraft_db_manager
self.main_window = main_window
module_logger.debug("AircraftDBImporter initialized.")
def import_aircraft_database_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
"""
Starts the aircraft database import process in a separate thread.
This method is intended to be called from the GUI thread.
Args:
csv_filepath: The path to the CSV file to import.
progress_dialog_ref: A reference to the ImportProgressDialog instance for updates.
"""
module_logger.info(
f"AircraftDBImporter: Requesting aircraft DB import with progress from: {csv_filepath}"
)
# Basic validation checks before starting the thread
if not self.aircraft_db_manager:
module_logger.error(
"AircraftDBImporter: AircraftDatabaseManager not initialized. Cannot import."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
# Schedule update to run on the GUI thread
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Error: Database manager not active.",
)
elif self.main_window and hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Database Error", "Aircraft database manager not active."
)
return
if not (self.main_window and self.main_window.root.winfo_exists()):
module_logger.error(
"AircraftDBImporter: MainWindow not available to start import thread."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Error: Main application window not available.",
)
return
# Notify the dialog that import has started
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(progress_dialog_ref.import_started)
# Start the import in a new daemon thread
import_thread = threading.Thread(
target=self._perform_db_import_threaded,
args=(csv_filepath, progress_dialog_ref),
daemon=True, # Daemon threads exit when the main program exits
name="AircraftDBImportWorker",
)
import_thread.start()
module_logger.info(
f"AircraftDBImporter: Import thread started for: {csv_filepath}"
)
def _perform_db_import_threaded(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
"""
Executes the database import process in a separate thread.
This method should NOT be called directly from the GUI thread.
"""
if not self.aircraft_db_manager:
module_logger.error(
"AircraftDBImporter: AircraftDBManager N/A in import thread."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Internal Error: DB Manager missing.",
)
return
# Attempt to increase CSV field size limit to handle large fields
self._set_csv_field_size_limit()
module_logger.info(
f"AircraftDBImporter: Import thread: Starting row count for: {csv_filepath}"
)
# Count total rows for progress bar calculation
total_data_rows = self._count_csv_rows(csv_filepath)
if total_data_rows is None:
module_logger.error(
f"AircraftDBImporter: Could not count rows in CSV: {csv_filepath}"
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.",
)
return
if total_data_rows == 0:
module_logger.info(
f"AircraftDBImporter: CSV file '{csv_filepath}' is empty or header-only."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
0,
f"File '{os.path.basename(csv_filepath)}' is empty or header-only.",
)
self._schedule_gui_update(
progress_dialog_ref.import_finished,
True,
"Import complete: No data rows to import.",
)
return
# Initial progress update with total rows found
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
total_data_rows,
f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...",
)
# Define the progress callback function that will be passed to the DB manager
def import_progress_update_callback(
processed_csv_rows, imported_db_rows, total_for_cb
):
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
# Schedule the GUI update on the main Tkinter thread
self._schedule_gui_update(
progress_dialog_ref.update_progress,
processed_csv_rows,
imported_db_rows,
total_for_cb,
f"Importing CSV row {processed_csv_rows}...",
)
# Perform the actual import via AircraftDatabaseManager
processed_final, imported_final = self.aircraft_db_manager.import_from_csv(
csv_filepath,
replace_existing=True, # Always replace existing for full import
progress_callback=import_progress_update_callback,
total_rows_for_callback=total_data_rows,
)
# Determine final message and success status
success = True
final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}."
if imported_final == 0 and processed_final > 0 and total_data_rows > 0:
success = False # Consider it a failure if no rows were imported
final_message = f"DB Import: {processed_final} CSV data rows processed, 0 imported (check CSV format/logs)."
module_logger.error(f"AircraftDBImporter: {final_message}")
elif imported_final == 0 and processed_final == 0 and total_data_rows > 0:
success = False
final_message = f"DB Import failed. Could not process rows from '{os.path.basename(csv_filepath)}'."
module_logger.critical(f"AircraftDBImporter: {final_message}")
# Final update to the progress dialog
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
processed_final,
imported_final,
total_data_rows,
"Finalizing import...",
)
self._schedule_gui_update(
progress_dialog_ref.import_finished, success, final_message
)
module_logger.info(
f"AircraftDBImporter: Import thread finished. Result: {final_message}"
)
def _count_csv_rows(self, csv_filepath: str) -> Optional[int]:
"""
Counts the number of data rows in a CSV file (excluding header).
Args:
csv_filepath: The path to the CSV file.
Returns:
The number of data rows, or None if the file cannot be read or is not found.
"""
# Temporarily increase CSV field size limit for robust row counting
self._set_csv_field_size_limit()
try:
with open(csv_filepath, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
try:
next(reader) # Skip header row
except StopIteration:
return 0 # File is empty or only contains header
row_count = sum(1 for _ in reader)
module_logger.debug(
f"AircraftDBImporter: Counted {row_count} data rows in {csv_filepath}"
)
return row_count
except FileNotFoundError:
module_logger.error(
f"AircraftDBImporter: CSV file not found: {csv_filepath}"
)
return None
except csv.Error as e_csv:
module_logger.error(
f"AircraftDBImporter: CSV format error during row count of {csv_filepath}: {e_csv}",
exc_info=False,
)
return None
except Exception as e:
module_logger.error(
f"AircraftDBImporter: Unexpected error counting rows in {csv_filepath}: {e}",
exc_info=True,
)
return None
def _set_csv_field_size_limit(self):
"""
Attempts to increase the CSV field size limit to handle potentially large fields.
"""
try:
current_limit = csv.field_size_limit()
if CSV_FIELD_SIZE_LIMIT_BYTES > current_limit:
csv.field_size_limit(CSV_FIELD_SIZE_LIMIT_BYTES)
module_logger.debug(
f"AircraftDBImporter: Increased csv.field_size_limit from {current_limit} to {CSV_FIELD_SIZE_LIMIT_BYTES}."
)
except Exception as e:
module_logger.warning(
f"AircraftDBImporter: Error attempting to increase csv.field_size_limit: {e}. Proceeding with default limit.",
exc_info=False,
)
def _schedule_gui_update(self, callable_func: Callable, *args: Any):
"""
Schedules a callable function to be executed on the main Tkinter thread.
This is crucial for safely updating GUI elements from a worker thread.
Args:
callable_func: The function to call on the GUI thread.
*args: Arguments to pass to the callable function.
"""
if (
self.main_window
and hasattr(self.main_window, "root")
and self.main_window.root.winfo_exists()
):
try:
self.main_window.root.after(0, lambda: callable_func(*args))
except Exception as e:
module_logger.error(
f"AircraftDBImporter: Error scheduling GUI update from import thread: {e}",
exc_info=True,
)
else:
module_logger.warning(
f"AircraftDBImporter: Main window or root destroyed, cannot schedule GUI update for {callable_func.__name__}."
)