Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true

# Python files
[*.py]
indent_style = space
indent_size = 4
max_line_length = 120

# YAML files
[*.{yml,yaml}]
indent_style = space
indent_size = 2

# JSON files
[*.json]
indent_style = space
indent_size = 2

# Markdown files
[*.md]
trim_trailing_whitespace = false

# Shell scripts
[*.sh]
indent_style = space
indent_size = 2

# Batch files
[*.bat]
end_of_line = crlf
22 changes: 22 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[flake8]
# Base configuration for flake8
max-line-length = 120
extend-ignore =
E203, # whitespace before ':'
E501, # line too long (handled by black)
W503, # line break before binary operator
E402, # module level import not at top of file (some files need conditional imports)
exclude =
.git,
__pycache__,
.venv,
venv,
build,
dist,
*.egg-info,
.tox,
docs,
http_cache,
per-file-ignores =
__init__.py: F401, F403
max-complexity = 15
97 changes: 97 additions & 0 deletions common/config_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
"""
Centralized configuration manager using Singleton pattern.

This module provides a thread-safe singleton wrapper around the existing
settings.Load class to ensure configuration is loaded once and reused
throughout the application lifecycle.
"""

import threading
from typing import Optional

from common.settings import Load, Config


class ConfigManager:
"""
Singleton configuration manager.

Ensures that configuration is loaded only once and provides
thread-safe access to the configuration object throughout
the application.

Usage:
config = ConfigManager.get_instance()
tracker_url = config.tracker_config.ITT_URL
"""

_instance: Optional['ConfigManager'] = None
_lock: threading.Lock = threading.Lock()
_config: Optional[Config] = None

def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
"""Initialize the configuration manager."""
if self._config is None:
with self._lock:
if self._config is None:
loader = Load()
self._config = loader.load_config()

@classmethod
def get_instance(cls) -> Config:
"""
Get the singleton configuration instance.

Returns:
Config: The application configuration object.
"""
manager = cls()
return manager._config

@classmethod
def reload(cls) -> Config:
"""
Force reload the configuration from disk.

Useful when configuration file has been modified and
needs to be reloaded without restarting the application.

Returns:
Config: The newly loaded configuration object.
"""
with cls._lock:
loader = Load()
cls._instance._config = loader.load_config()
return cls._instance._config

@property
def config(self) -> Config:
"""Get the current configuration."""
return self._config


# Convenience function for quick access
def get_config() -> Config:
"""
Get the application configuration.

This is a convenience function that returns the singleton
configuration instance.

Returns:
Config: The application configuration object.

Example:
>>> from common.config_manager import get_config
>>> config = get_config()
>>> print(config.tracker_config.ITT_URL)
"""
return ConfigManager.get_instance()
160 changes: 160 additions & 0 deletions common/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""
Structured logging module for Unit3Dup.

Provides consistent logging across the application with proper
levels, formatting, and error handling.
"""

import logging
import sys
from pathlib import Path
from typing import Optional
from datetime import datetime


class Unit3DupLogger:
"""
Custom logger for Unit3Dup application.

Provides structured logging with console and optional file output.
"""

_loggers = {}

@classmethod
def get_logger(cls, name: str = "unit3dup", log_file: Optional[Path] = None) -> logging.Logger:
"""
Get or create a logger instance.

Args:
name: Logger name
log_file: Optional path to log file

Returns:
Configured logger instance
"""
if name in cls._loggers:
return cls._loggers[name]

logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)

# Avoid duplicate handlers
if logger.handlers:
return logger

# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)

# File handler (optional)
if log_file:
log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)

cls._loggers[name] = logger
return logger


class ExitCodes:
"""Standard exit codes for the application."""

SUCCESS = 0
GENERAL_ERROR = 1
CONFIG_ERROR = 2
CONNECTION_ERROR = 3
VALIDATION_ERROR = 4
FILE_NOT_FOUND = 5


def safe_exit(message: str, exit_code: int = ExitCodes.GENERAL_ERROR, logger: Optional[logging.Logger] = None) -> None:
"""
Safely exit the application with proper logging.

Args:
message: Error message to display
exit_code: Exit code (default: 1)
logger: Optional logger instance
"""
if logger:
logger.error(message)
else:
# Fallback to stderr if no logger provided
print(f"ERROR: {message}", file=sys.stderr)

sys.exit(exit_code)


class LogContext:
"""Context manager for logging operations with automatic error handling."""

def __init__(self, operation: str, logger: logging.Logger, raise_on_error: bool = False):
"""
Initialize log context.

Args:
operation: Description of the operation
logger: Logger instance
raise_on_error: Whether to re-raise exceptions
"""
self.operation = operation
self.logger = logger
self.raise_on_error = raise_on_error
self.start_time = None

def __enter__(self):
"""Enter context."""
self.start_time = datetime.now()
self.logger.info(f"Starting: {self.operation}")
return self

def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context with error handling."""
duration = (datetime.now() - self.start_time).total_seconds()

if exc_type is None:
self.logger.info(f"Completed: {self.operation} (took {duration:.2f}s)")
return True

self.logger.error(
f"Failed: {self.operation} (after {duration:.2f}s) - {exc_type.__name__}: {exc_val}"
)

if self.raise_on_error:
return False # Re-raise the exception

return True # Suppress the exception


# Convenience function
def get_logger(name: str = "unit3dup", log_file: Optional[Path] = None) -> logging.Logger:
"""
Get a logger instance.

Args:
name: Logger name
log_file: Optional path to log file

Returns:
Logger instance

Example:
>>> from common.logger import get_logger
>>> logger = get_logger(__name__)
>>> logger.info("Starting upload process")
"""
return Unit3DupLogger.get_logger(name, log_file)
17 changes: 14 additions & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
[mypy]
python_version = 3.10
files = common/
disallow_untyped_defs = True
files = common/, unit3dup/, view/
warn_return_any = True
warn_unused_configs = True
warn_redundant_casts = True
warn_unused_ignores = True
disallow_untyped_defs = False
check_untyped_defs = True
no_implicit_optional = True
strict_equality = True
ignore_missing_imports = True
exclude = ^(tests|build|docs|dist|http_cache|Pw)
exclude = ^(tests|build|docs|dist|http_cache|venv)

# Per-module options:
[mypy-tests.*]
ignore_errors = True
Loading