Files
AirlockTools/utils/setup.py
Zarithas e1e0cb7ac7 feat: add version updater and statistics enhancements (fixes #29)
- Implemented version checking system with update notifications
- Integrated Git for fetching and downloading the latest version
- Added statistics updates
- Removed unused code across the project
- Condensed project structure
- Updated README
- Cleaned up UI
2025-12-19 16:31:36 -05:00

237 lines
8.4 KiB
Python

# Copyright (C) 2025 James Brotosky, Brandon Wickline
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import logging.config
import logging.handlers
import os
from pathlib import Path
import platform
from dotenv import load_dotenv, set_key
from utils.configmanager import (
get_system_value,
load_system_config,
load_user_config,
)
class TextualNotificationHandler(logging.Handler):
"""
Custom logging handler that sends ERROR, WARNING, and CRITICAL logs
to Textual toast notifications.
"""
def __init__(self, app):
super().__init__()
self.app = app
def emit(self, record):
try:
# Only handle ERROR, WARNING, and CRITICAL
if record.levelno >= logging.WARNING:
# Format the message
msg = self.format(record)
# Map log levels to Textual severity
severity_map = {
logging.WARNING: "warning",
logging.ERROR: "error",
logging.CRITICAL: "error",
}
severity = severity_map.get(record.levelno, "information")
# Send to Textual notification
# Use call_from_thread if logging from non-main thread
try:
self.app.notify(msg, severity=severity, timeout=5)
except Exception:
# If we're not on the main thread, schedule it
try:
self.app.call_from_thread(
self.app.notify, msg, severity=severity, timeout=5
)
except Exception:
# Silently fail to avoid breaking the logging system
pass
except Exception:
# Silently fail to avoid breaking the logging system
pass
def get_base_directory() -> Path:
system = platform.system()
home = Path.home()
if system == "Windows":
return Path(os.getenv("APPDATA", home / "AppData" / "Roaming")) / "Loxide"
elif system == "Darwin":
return home / "Library" / "Application Support" / "Loxide"
else:
return home / ".local" / "share" / "Loxide"
def configure_logging(log_dir: Path, cache_dir: Path, log_level: str = "INFO"):
"""
Configure logging and return function to attach notification handler.
Args:
log_dir: Directory for log files
cache_dir: Directory for cache files (used by version checker)
log_level: Logging level string
Returns:
Function to attach notification handler to Textual app
"""
log_file = log_dir / "Loxide.log"
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
},
"simple": {"format": "%(levelname)s - %(message)s"},
"toast": {"format": "%(name)s: %(message)s"}, # Simpler format for toasts
},
"handlers": {
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": str(log_file),
"when": "midnight",
"interval": 1,
"backupCount": 7,
"encoding": "utf-8",
"level": "DEBUG",
"formatter": "detailed",
},
# REMOVED console handler - it interferes with Textual TUI
},
"root": {
"level": "DEBUG",
"handlers": ["file"], # Only use file handler, not console
},
}
# Add Windows Event Log handler if on Windows
if platform.system() == "Windows":
try:
config["handlers"]["eventlog"] = {
"class": "logging.handlers.NTEventLogHandler",
"appname": "Loxide", # Event log source name
"level": "CRITICAL", # Only log critical errors
"formatter": "simple", # Use simple format
}
config["root"]["handlers"].append("eventlog")
except Exception as e:
logging.warning(f"Could not attach Windows Event Log handler: {e}")
# Apply the logging configuration
logging.config.dictConfig(config)
logging.getLogger().debug("✅ Logging configured.")
# Return a function to attach the notification handler once the app is created
def attach_notification_handler(app):
"""Attach the Textual notification handler and version checker to the app."""
# Attach logging handler
handler = TextualNotificationHandler(app)
handler.setLevel(logging.WARNING) # Only WARNING and above
formatter = logging.Formatter("%(name)s: %(message)s")
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logging.getLogger().debug("✅ Textual notification handler attached.")
# Attach version checker (checks in background, notifies if update available)
try:
from utils.versionchecker import create_update_notifier
create_update_notifier(app, cache_dir=cache_dir)
logging.getLogger().debug("✅ Version checker attached.")
except ImportError as e:
logging.getLogger().debug(f"Version checker not available: {e}")
except Exception as e:
logging.getLogger().warning(f"Could not initialize version checker: {e}")
return attach_notification_handler
def setup():
"""
Initialize the application environment:
1. Create directory structure
2. Load system config (immutable, from system_config.json)
3. Load user config (mutable, from user_config.json)
4. Configure logging
5. Set up .env with WORKING_DIR only
Returns:
attach_notification_handler: Function to attach notification handler to TUI app
"""
base_dir = get_base_directory()
dirs = {
"config": base_dir / "config",
"cache": base_dir / "cache",
"logs": base_dir / "logs",
}
for name, path in dirs.items():
path.mkdir(parents=True, exist_ok=True)
logging.debug(f"{name.capitalize()} directory ensured at: {path}")
# Load system config (immutable)
load_system_config()
# Configure logging with system-defined log level
log_level = get_system_value("LOG_LEVEL", str, "INFO")
attach_handler = configure_logging(dirs["logs"], dirs["cache"], log_level)
# Load user config (mutable)
load_user_config(dirs["config"])
# Set up .env file - ONLY for WORKING_DIR (runtime-configurable value)
env_path = base_dir / ".env"
if not env_path.exists():
env_path.touch()
load_dotenv(dotenv_path=env_path, override=True)
# Set up working directory (only dynamic value in .env)
working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data"))
working_dir.mkdir(parents=True, exist_ok=True)
set_key(str(env_path), "WORKING_DIR", str(working_dir))
os.environ["WORKING_DIR"] = str(working_dir)
logging.debug(f"Working directory set to: {working_dir}")
# Create folder structure in working directory
folders_structure = {
"Approved": [],
"Needs_Review": ["Review_First", "Review_Second", "HTML"],
"Preflight": ["HTML"],
}
for folder_name, subfolders in folders_structure.items():
folder_path = working_dir / folder_name
folder_path.mkdir(parents=True, exist_ok=True)
logging.debug(f"'{folder_name}' folder ensured at: {folder_path}")
for subfolder in subfolders:
subfolder_path = folder_path / subfolder
subfolder_path.mkdir(parents=True, exist_ok=True)
logging.debug(f"'{subfolder}' subfolder created at: {subfolder_path}")
logging.info("✅ Setup complete")
# Return the attach handler function
return attach_handler