Asset-Frameworker/.lh/configuration.py.json
2025-04-29 18:26:13 +02:00

26 lines
65 KiB
JSON

{
"sourceFile": "configuration.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 2,
"patches": [
{
"date": 1745314026627,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1745315881107,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -90,9 +90,8 @@\n self.preset_name = preset_name\n self._core_settings: dict = self._load_core_config()\n self._preset_settings: dict = self._load_preset(preset_name)\n self._validate_configs()\n- log.debug(f\"Loaded source_naming config: {self._preset_settings.get('source_naming', 'Not Found')}\") # DEBUG: Log loaded source_naming\n self._compile_regex_patterns() # Compile regex after validation\n log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n \n \n"
},
{
"date": 1745506645928,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,418 +1,418 @@\n-# configuration.py\n-\n-import json\n-import os\n-import importlib.util\n-from pathlib import Path\n-import logging\n-import re # Import the regex module\n-\n-log = logging.getLogger(__name__) # Use logger defined in main.py\n-\n-# --- Constants ---\n-# Assumes config.py and presets/ are relative to this file's location\n-BASE_DIR = Path(__file__).parent\n-CORE_CONFIG_PATH = BASE_DIR / \"config.py\"\n-PRESETS_DIR = BASE_DIR / \"presets\"\n-\n-# --- Custom Exception ---\n-class ConfigurationError(Exception):\n- \"\"\"Custom exception for configuration loading errors.\"\"\"\n- pass\n-\n-# --- Helper Functions ---\n-def _get_base_map_type(target_map_string: str) -> str:\n- \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\n- # Use regex to find the leading alphabetical part\n- match = re.match(r\"([a-zA-Z]+)\", target_map_string)\n- if match:\n- return match.group(1).upper()\n- # Fallback if no number suffix or unexpected format\n- return target_map_string.upper()\n-\n-def _fnmatch_to_regex(pattern: str) -> str:\n- \"\"\"\n- Converts an fnmatch pattern to a regex pattern string.\n- Handles basic wildcards (*, ?) and escapes other regex special characters.\n- \"\"\"\n- i, n = 0, len(pattern)\n- res = ''\n- while i < n:\n- c = pattern[i]\n- i = i + 1\n- if c == '*':\n- res = res + '.*'\n- elif c == '?':\n- res = res + '.'\n- elif c == '[':\n- j = i\n- if j < n and pattern[j] == '!':\n- j = j + 1\n- if j < n and pattern[j] == ']':\n- j = j + 1\n- while j < n and pattern[j] != ']':\n- j = j + 1\n- if j >= n:\n- res = res + '\\\\['\n- else:\n- stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\n- i = j + 1\n- if stuff[0] == '!':\n- stuff = '^' + stuff[1:]\n- elif stuff[0] == '^':\n- stuff = '\\\\' + stuff\n- res = '%s[%s]' % (res, stuff)\n- else:\n- res = res + re.escape(c)\n- # We want to find the pattern anywhere in the filename for flexibility,\n- # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\n- # Let's return the core pattern and let the caller decide on anchoring if needed.\n- # For filename matching, we usually want to find the pattern, not match the whole string.\n- return res\n-\n-\n-# --- Configuration Class ---\n-class Configuration:\n- \"\"\"\n- Loads and provides access to core settings combined with a specific preset.\n- \"\"\"\n- def __init__(self, preset_name: str):\n- \"\"\"\n- Loads core config and the specified preset file.\n-\n- Args:\n- preset_name: The name of the preset (without .json extension).\n-\n- Raises:\n- ConfigurationError: If core config or preset cannot be loaded/validated.\n- \"\"\"\n- log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\n- self.preset_name = preset_name\n- self._core_settings: dict = self._load_core_config()\n- self._preset_settings: dict = self._load_preset(preset_name)\n- self._validate_configs()\n- self._compile_regex_patterns() # Compile regex after validation\n- log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n-\n-\n- def _compile_regex_patterns(self):\n- \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\n- log.debug(\"Compiling regex patterns from configuration...\")\n- self.compiled_extra_regex: list[re.Pattern] = []\n- self.compiled_model_regex: list[re.Pattern] = []\n- self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\n- # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\n- self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\n- # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\n- # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\n-\n- # Compile Extra Patterns (case-insensitive)\n- for pattern in self.move_to_extra_patterns:\n- try:\n- # Use the raw fnmatch pattern directly if it's simple enough for re.search\n- # Or convert using helper if needed. Let's try direct search first.\n- # We want to find the pattern *within* the filename.\n- regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\n- self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\n- except re.error as e:\n- log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\n-\n- # Compile Model Patterns (case-insensitive)\n- model_patterns = self.asset_category_rules.get('model_patterns', [])\n- for pattern in model_patterns:\n- try:\n- regex_str = _fnmatch_to_regex(pattern)\n- self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\n- except re.error as e:\n- log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\n-\n- # Compile Bit Depth Variant Patterns (case-sensitive recommended)\n- for map_type, pattern in self.source_bit_depth_variants.items():\n- try:\n- # These often rely on specific suffixes, so anchoring might be better?\n- # Let's stick to the converted pattern for now, assuming it ends with suffix.\n- regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\n- # If the original pattern ended with *, remove the trailing '.*' for suffix matching\n- if pattern.endswith('*'):\n- regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\n- # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\n-\n- # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\n- # This is less strict than anchoring to the end with \\\\.[^.]+$\n- final_regex_str = regex_str # Use the result from _fnmatch_to_regex\n- self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\n- log.debug(f\" Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\n- except re.error as e:\n- log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\n-\n- # Compile Map Type Keywords (case-insensitive) based on the new structure\n- separator = re.escape(self.source_naming_separator) # Escape separator for regex\n- # Use defaultdict to easily append to lists for the same base type\n- from collections import defaultdict\n- temp_compiled_map_regex = defaultdict(list)\n-\n- for rule_index, mapping_rule in enumerate(self.map_type_mapping):\n- # Validate rule structure (dictionary with target_type and keywords)\n- if not isinstance(mapping_rule, dict) or \\\n- 'target_type' not in mapping_rule or \\\n- 'keywords' not in mapping_rule or \\\n- not isinstance(mapping_rule['keywords'], list):\n- log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\n- continue\n-\n- target_type = mapping_rule['target_type'].upper() # Use the base type directly\n- source_keywords = mapping_rule['keywords']\n-\n- # Store the rule for potential priority access later (optional, index is now in tuple)\n- # self._map_type_rule_order.append(mapping_rule)\n-\n- if target_type not in self.standard_map_types:\n- log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\n- # Continue processing anyway, as it might be intended for merging etc.\n-\n- # Compile keywords for this rule and store with context\n- for keyword in source_keywords:\n- if not isinstance(keyword, str):\n- log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\n- continue\n- try:\n- # Match keyword potentially surrounded by separators or start/end\n- # Handle potential wildcards within the keyword using fnmatch conversion\n- kw_regex_part = _fnmatch_to_regex(keyword)\n- # Build regex to match the keyword part, anchored by separators or string boundaries\n- # Use non-capturing groups (?:...)\n- # Capture the keyword part itself for potential use later if needed (group 1)\n- regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\n- compiled_regex = re.compile(regex_str, re.IGNORECASE)\n- # Append tuple: (compiled_regex, original_keyword, rule_index)\n- temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\n- log.debug(f\" Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\n- except re.error as e:\n- log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\n-\n- # Assign the compiled regex dictionary\n- self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\n- log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\n-\n- log.debug(\"Finished compiling regex patterns.\")\n-\n-\n- def _load_core_config(self) -> dict:\n- \"\"\"Loads settings from the core config.py file.\"\"\"\n- log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\n- if not CORE_CONFIG_PATH.is_file():\n- raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\n- try:\n- spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\n- if spec is None or spec.loader is None:\n- raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\n- core_config_module = importlib.util.module_from_spec(spec)\n- # Define default values for core settings in case they are missing in config.py\n- default_core_settings = {\n- 'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\n- 'STANDARD_MAP_TYPES': [],\n- 'EXTRA_FILES_SUBDIR': \"Extra\",\n- 'METADATA_FILENAME': \"metadata.json\",\n- 'IMAGE_RESOLUTIONS': {},\n- 'ASPECT_RATIO_DECIMALS': 2,\n- 'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\n- 'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\n- 'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\n- 'OUTPUT_FORMAT_8BIT': \"png\",\n- 'MAP_MERGE_RULES': [],\n- 'CALCULATE_STATS_RESOLUTION': \"1K\",\n- 'DEFAULT_ASSET_CATEGORY': \"Texture\",\n- 'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\n- # --- Additions ---\n- 'JPG_QUALITY': 95, # Default JPG quality\n- 'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\n- 'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\n- 'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\n- }\n- # Load attributes from module, using defaults if missing\n- settings = default_core_settings.copy()\n- spec.loader.exec_module(core_config_module)\n- for name in default_core_settings:\n- if hasattr(core_config_module, name):\n- settings[name] = getattr(core_config_module, name)\n-\n- log.debug(f\"Core config loaded successfully.\")\n- return settings\n- except Exception as e:\n- raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\n-\n- def _load_preset(self, preset_name: str) -> dict:\n- \"\"\"Loads the specified preset JSON file.\"\"\"\n- log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\n- if not PRESETS_DIR.is_dir():\n- raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\n-\n- preset_file = PRESETS_DIR / f\"{preset_name}.json\"\n- if not preset_file.is_file():\n- raise ConfigurationError(f\"Preset file not found: {preset_file}\")\n-\n- try:\n- with open(preset_file, 'r', encoding='utf-8') as f:\n- preset_data = json.load(f)\n- log.debug(f\"Preset '{preset_name}' loaded successfully.\")\n- return preset_data\n- except json.JSONDecodeError as e:\n- raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\n- except Exception as e:\n- raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\n-\n- def _validate_configs(self):\n- \"\"\"Performs basic validation checks on loaded settings.\"\"\"\n- log.debug(\"Validating loaded configurations...\")\n- # Preset validation\n- required_preset_keys = [\n- \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\n- \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\n- ]\n- for key in required_preset_keys:\n- if key not in self._preset_settings:\n- raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\n-\n- # Validate map_type_mapping structure (new format)\n- if not isinstance(self._preset_settings['map_type_mapping'], list):\n- raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\n- for index, rule in enumerate(self._preset_settings['map_type_mapping']):\n- if not isinstance(rule, dict):\n- raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\n- if 'target_type' not in rule or not isinstance(rule['target_type'], str):\n- raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\n- if 'keywords' not in rule or not isinstance(rule['keywords'], list):\n- raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\n- for kw_index, keyword in enumerate(rule['keywords']):\n- if not isinstance(keyword, str):\n- raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\n-\n-\n- # Core validation (check types or specific values if needed)\n- if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\n- raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\n- if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\n- raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\n- if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\n- raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\n- # Add more checks as necessary\n- log.debug(\"Configuration validation passed.\")\n-\n-\n- # --- Accessor Methods/Properties ---\n- # Use @property for direct access, methods for potentially complex lookups/defaults\n-\n- @property\n- def supplier_name(self) -> str:\n- return self._preset_settings.get('supplier_name', 'DefaultSupplier')\n- \n- @property\n- def default_asset_category(self) -> str:\n- \"\"\"Gets the default asset category from core settings.\"\"\"\n- # Provide a fallback default just in case it's missing from config.py\n- return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\n-\n- @property\n- def target_filename_pattern(self) -> str:\n- return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\n-\n- @property\n- def image_resolutions(self) -> dict[str, int]:\n- return self._core_settings['IMAGE_RESOLUTIONS']\n-\n- @property\n- def standard_map_types(self) -> list[str]:\n- return self._core_settings['STANDARD_MAP_TYPES']\n-\n- @property\n- def map_type_mapping(self) -> list:\n- return self._preset_settings['map_type_mapping']\n-\n- @property\n- def source_naming_separator(self) -> str:\n- return self._preset_settings.get('source_naming', {}).get('separator', '_')\n-\n- @property\n- def source_naming_indices(self) -> dict:\n- return self._preset_settings.get('source_naming', {}).get('part_indices', {})\n-\n- @property\n- def source_glossiness_keywords(self) -> list:\n- return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\n-\n- @property\n- def source_bit_depth_variants(self) -> dict:\n- return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\n-\n- @property\n- def archetype_rules(self) -> list:\n- return self._preset_settings['archetype_rules']\n-\n- @property\n- def asset_category_rules(self) -> dict:\n- return self._preset_settings['asset_category_rules']\n-\n- @property\n- def move_to_extra_patterns(self) -> list:\n- return self._preset_settings['move_to_extra_patterns']\n-\n- @property\n- def extra_files_subdir(self) -> str:\n- return self._core_settings['EXTRA_FILES_SUBDIR']\n-\n- @property\n- def metadata_filename(self) -> str:\n- return self._core_settings['METADATA_FILENAME']\n-\n- @property\n- def calculate_stats_resolution(self) -> str:\n- return self._core_settings['CALCULATE_STATS_RESOLUTION']\n-\n- @property\n- def map_merge_rules(self) -> list:\n- return self._core_settings['MAP_MERGE_RULES']\n-\n- @property\n- def aspect_ratio_decimals(self) -> int:\n- return self._core_settings['ASPECT_RATIO_DECIMALS']\n-\n- @property\n- def temp_dir_prefix(self) -> str:\n- return self._core_settings['TEMP_DIR_PREFIX']\n-\n- @property\n- def jpg_quality(self) -> int:\n- \"\"\"Gets the configured JPG quality level.\"\"\"\n- return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\n-\n- @property\n- def resolution_threshold_for_jpg(self) -> int:\n- \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\n- return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\n-\n- @property\n- def respect_variant_map_types(self) -> list:\n- \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\n- # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\n- return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\n-\n- @property\n- def force_lossless_map_types(self) -> list:\n- \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\n- return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\n-\n- def get_bit_depth_rule(self, map_type: str) -> str:\n- \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\n- rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\n- default_rule = rules.get('DEFAULT', 'respect')\n- return rules.get(map_type, default_rule)\n-\n- def get_16bit_output_formats(self) -> tuple[str, str]:\n- \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\n- primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\n- fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\n- return primary.lower(), fallback.lower()\n-\n- def get_8bit_output_format(self) -> str:\n- \"\"\"Gets the format name for 8-bit output.\"\"\"\n+# configuration.py\r\n+\r\n+import json\r\n+import os\r\n+import importlib.util\r\n+from pathlib import Path\r\n+import logging\r\n+import re # Import the regex module\r\n+\r\n+log = logging.getLogger(__name__) # Use logger defined in main.py\r\n+\r\n+# --- Constants ---\r\n+# Assumes config.py and presets/ are relative to this file's location\r\n+BASE_DIR = Path(__file__).parent\r\n+CORE_CONFIG_PATH = BASE_DIR / \"config.py\"\r\n+PRESETS_DIR = BASE_DIR / \"presets\"\r\n+\r\n+# --- Custom Exception ---\r\n+class ConfigurationError(Exception):\r\n+ \"\"\"Custom exception for configuration loading errors.\"\"\"\r\n+ pass\r\n+\r\n+# --- Helper Functions ---\r\n+def _get_base_map_type(target_map_string: str) -> str:\r\n+ \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\r\n+ # Use regex to find the leading alphabetical part\r\n+ match = re.match(r\"([a-zA-Z]+)\", target_map_string)\r\n+ if match:\r\n+ return match.group(1).upper()\r\n+ # Fallback if no number suffix or unexpected format\r\n+ return target_map_string.upper()\r\n+\r\n+def _fnmatch_to_regex(pattern: str) -> str:\r\n+ \"\"\"\r\n+ Converts an fnmatch pattern to a regex pattern string.\r\n+ Handles basic wildcards (*, ?) and escapes other regex special characters.\r\n+ \"\"\"\r\n+ i, n = 0, len(pattern)\r\n+ res = ''\r\n+ while i < n:\r\n+ c = pattern[i]\r\n+ i = i + 1\r\n+ if c == '*':\r\n+ res = res + '.*'\r\n+ elif c == '?':\r\n+ res = res + '.'\r\n+ elif c == '[':\r\n+ j = i\r\n+ if j < n and pattern[j] == '!':\r\n+ j = j + 1\r\n+ if j < n and pattern[j] == ']':\r\n+ j = j + 1\r\n+ while j < n and pattern[j] != ']':\r\n+ j = j + 1\r\n+ if j >= n:\r\n+ res = res + '\\\\['\r\n+ else:\r\n+ stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\r\n+ i = j + 1\r\n+ if stuff[0] == '!':\r\n+ stuff = '^' + stuff[1:]\r\n+ elif stuff[0] == '^':\r\n+ stuff = '\\\\' + stuff\r\n+ res = '%s[%s]' % (res, stuff)\r\n+ else:\r\n+ res = res + re.escape(c)\r\n+ # We want to find the pattern anywhere in the filename for flexibility,\r\n+ # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\r\n+ # Let's return the core pattern and let the caller decide on anchoring if needed.\r\n+ # For filename matching, we usually want to find the pattern, not match the whole string.\r\n+ return res\r\n+\r\n+\r\n+# --- Configuration Class ---\r\n+class Configuration:\r\n+ \"\"\"\r\n+ Loads and provides access to core settings combined with a specific preset.\r\n+ \"\"\"\r\n+ def __init__(self, preset_name: str):\r\n+ \"\"\"\r\n+ Loads core config and the specified preset file.\r\n+\r\n+ Args:\r\n+ preset_name: The name of the preset (without .json extension).\r\n+\r\n+ Raises:\r\n+ ConfigurationError: If core config or preset cannot be loaded/validated.\r\n+ \"\"\"\r\n+ log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\r\n+ self.preset_name = preset_name\r\n+ self._core_settings: dict = self._load_core_config()\r\n+ self._preset_settings: dict = self._load_preset(preset_name)\r\n+ self._validate_configs()\r\n+ self._compile_regex_patterns() # Compile regex after validation\r\n+ log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\r\n+\r\n+\r\n+ def _compile_regex_patterns(self):\r\n+ \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\r\n+ log.debug(\"Compiling regex patterns from configuration...\")\r\n+ self.compiled_extra_regex: list[re.Pattern] = []\r\n+ self.compiled_model_regex: list[re.Pattern] = []\r\n+ self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\r\n+ # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\r\n+ self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\r\n+ # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\r\n+ # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\r\n+\r\n+ # Compile Extra Patterns (case-insensitive)\r\n+ for pattern in self.move_to_extra_patterns:\r\n+ try:\r\n+ # Use the raw fnmatch pattern directly if it's simple enough for re.search\r\n+ # Or convert using helper if needed. Let's try direct search first.\r\n+ # We want to find the pattern *within* the filename.\r\n+ regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\r\n+ self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\r\n+ except re.error as e:\r\n+ log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\r\n+\r\n+ # Compile Model Patterns (case-insensitive)\r\n+ model_patterns = self.asset_category_rules.get('model_patterns', [])\r\n+ for pattern in model_patterns:\r\n+ try:\r\n+ regex_str = _fnmatch_to_regex(pattern)\r\n+ self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\r\n+ except re.error as e:\r\n+ log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\r\n+\r\n+ # Compile Bit Depth Variant Patterns (case-sensitive recommended)\r\n+ for map_type, pattern in self.source_bit_depth_variants.items():\r\n+ try:\r\n+ # These often rely on specific suffixes, so anchoring might be better?\r\n+ # Let's stick to the converted pattern for now, assuming it ends with suffix.\r\n+ regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\r\n+ # If the original pattern ended with *, remove the trailing '.*' for suffix matching\r\n+ if pattern.endswith('*'):\r\n+ regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\r\n+ # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\r\n+\r\n+ # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\r\n+ # This is less strict than anchoring to the end with \\\\.[^.]+$\r\n+ final_regex_str = regex_str # Use the result from _fnmatch_to_regex\r\n+ self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\r\n+ log.debug(f\" Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\r\n+ except re.error as e:\r\n+ log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\r\n+\r\n+ # Compile Map Type Keywords (case-insensitive) based on the new structure\r\n+ separator = re.escape(self.source_naming_separator) # Escape separator for regex\r\n+ # Use defaultdict to easily append to lists for the same base type\r\n+ from collections import defaultdict\r\n+ temp_compiled_map_regex = defaultdict(list)\r\n+\r\n+ for rule_index, mapping_rule in enumerate(self.map_type_mapping):\r\n+ # Validate rule structure (dictionary with target_type and keywords)\r\n+ if not isinstance(mapping_rule, dict) or \\\r\n+ 'target_type' not in mapping_rule or \\\r\n+ 'keywords' not in mapping_rule or \\\r\n+ not isinstance(mapping_rule['keywords'], list):\r\n+ log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\r\n+ continue\r\n+\r\n+ target_type = mapping_rule['target_type'].upper() # Use the base type directly\r\n+ source_keywords = mapping_rule['keywords']\r\n+\r\n+ # Store the rule for potential priority access later (optional, index is now in tuple)\r\n+ # self._map_type_rule_order.append(mapping_rule)\r\n+\r\n+ if target_type not in self.standard_map_types:\r\n+ log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\r\n+ # Continue processing anyway, as it might be intended for merging etc.\r\n+\r\n+ # Compile keywords for this rule and store with context\r\n+ for keyword in source_keywords:\r\n+ if not isinstance(keyword, str):\r\n+ log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\r\n+ continue\r\n+ try:\r\n+ # Match keyword potentially surrounded by separators or start/end\r\n+ # Handle potential wildcards within the keyword using fnmatch conversion\r\n+ kw_regex_part = _fnmatch_to_regex(keyword)\r\n+ # Build regex to match the keyword part, anchored by separators or string boundaries\r\n+ # Use non-capturing groups (?:...)\r\n+ # Capture the keyword part itself for potential use later if needed (group 1)\r\n+ regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\r\n+ compiled_regex = re.compile(regex_str, re.IGNORECASE)\r\n+ # Append tuple: (compiled_regex, original_keyword, rule_index)\r\n+ temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\r\n+ log.debug(f\" Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\r\n+ except re.error as e:\r\n+ log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\r\n+\r\n+ # Assign the compiled regex dictionary\r\n+ self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\r\n+ log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\r\n+\r\n+ log.debug(\"Finished compiling regex patterns.\")\r\n+\r\n+\r\n+ def _load_core_config(self) -> dict:\r\n+ \"\"\"Loads settings from the core config.py file.\"\"\"\r\n+ log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\r\n+ if not CORE_CONFIG_PATH.is_file():\r\n+ raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\r\n+ try:\r\n+ spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\r\n+ if spec is None or spec.loader is None:\r\n+ raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\r\n+ core_config_module = importlib.util.module_from_spec(spec)\r\n+ # Define default values for core settings in case they are missing in config.py\r\n+ default_core_settings = {\r\n+ 'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\r\n+ 'STANDARD_MAP_TYPES': [],\r\n+ 'EXTRA_FILES_SUBDIR': \"Extra\",\r\n+ 'METADATA_FILENAME': \"metadata.json\",\r\n+ 'IMAGE_RESOLUTIONS': {},\r\n+ 'ASPECT_RATIO_DECIMALS': 2,\r\n+ 'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\r\n+ 'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\r\n+ 'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\r\n+ 'OUTPUT_FORMAT_8BIT': \"png\",\r\n+ 'MAP_MERGE_RULES': [],\r\n+ 'CALCULATE_STATS_RESOLUTION': \"1K\",\r\n+ 'DEFAULT_ASSET_CATEGORY': \"Texture\",\r\n+ 'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\r\n+ # --- Additions ---\r\n+ 'JPG_QUALITY': 95, # Default JPG quality\r\n+ 'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\r\n+ 'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\r\n+ 'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\r\n+ }\r\n+ # Load attributes from module, using defaults if missing\r\n+ settings = default_core_settings.copy()\r\n+ spec.loader.exec_module(core_config_module)\r\n+ for name in default_core_settings:\r\n+ if hasattr(core_config_module, name):\r\n+ settings[name] = getattr(core_config_module, name)\r\n+\r\n+ log.debug(f\"Core config loaded successfully.\")\r\n+ return settings\r\n+ except Exception as e:\r\n+ raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\r\n+\r\n+ def _load_preset(self, preset_name: str) -> dict:\r\n+ \"\"\"Loads the specified preset JSON file.\"\"\"\r\n+ log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\r\n+ if not PRESETS_DIR.is_dir():\r\n+ raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\r\n+\r\n+ preset_file = PRESETS_DIR / f\"{preset_name}.json\"\r\n+ if not preset_file.is_file():\r\n+ raise ConfigurationError(f\"Preset file not found: {preset_file}\")\r\n+\r\n+ try:\r\n+ with open(preset_file, 'r', encoding='utf-8') as f:\r\n+ preset_data = json.load(f)\r\n+ log.debug(f\"Preset '{preset_name}' loaded successfully.\")\r\n+ return preset_data\r\n+ except json.JSONDecodeError as e:\r\n+ raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\r\n+ except Exception as e:\r\n+ raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\r\n+\r\n+ def _validate_configs(self):\r\n+ \"\"\"Performs basic validation checks on loaded settings.\"\"\"\r\n+ log.debug(\"Validating loaded configurations...\")\r\n+ # Preset validation\r\n+ required_preset_keys = [\r\n+ \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\r\n+ \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\r\n+ ]\r\n+ for key in required_preset_keys:\r\n+ if key not in self._preset_settings:\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\r\n+\r\n+ # Validate map_type_mapping structure (new format)\r\n+ if not isinstance(self._preset_settings['map_type_mapping'], list):\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\r\n+ for index, rule in enumerate(self._preset_settings['map_type_mapping']):\r\n+ if not isinstance(rule, dict):\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\r\n+ if 'target_type' not in rule or not isinstance(rule['target_type'], str):\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\r\n+ if 'keywords' not in rule or not isinstance(rule['keywords'], list):\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\r\n+ for kw_index, keyword in enumerate(rule['keywords']):\r\n+ if not isinstance(keyword, str):\r\n+ raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\r\n+\r\n+\r\n+ # Core validation (check types or specific values if needed)\r\n+ if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\r\n+ raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\r\n+ if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\r\n+ raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\r\n+ if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\r\n+ raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\r\n+ # Add more checks as necessary\r\n+ log.debug(\"Configuration validation passed.\")\r\n+\r\n+\r\n+ # --- Accessor Methods/Properties ---\r\n+ # Use @property for direct access, methods for potentially complex lookups/defaults\r\n+\r\n+ @property\r\n+ def supplier_name(self) -> str:\r\n+ return self._preset_settings.get('supplier_name', 'DefaultSupplier')\r\n+ \r\n+ @property\r\n+ def default_asset_category(self) -> str:\r\n+ \"\"\"Gets the default asset category from core settings.\"\"\"\r\n+ # Provide a fallback default just in case it's missing from config.py\r\n+ return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\r\n+\r\n+ @property\r\n+ def target_filename_pattern(self) -> str:\r\n+ return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\r\n+\r\n+ @property\r\n+ def image_resolutions(self) -> dict[str, int]:\r\n+ return self._core_settings['IMAGE_RESOLUTIONS']\r\n+\r\n+ @property\r\n+ def standard_map_types(self) -> list[str]:\r\n+ return self._core_settings['STANDARD_MAP_TYPES']\r\n+\r\n+ @property\r\n+ def map_type_mapping(self) -> list:\r\n+ return self._preset_settings['map_type_mapping']\r\n+\r\n+ @property\r\n+ def source_naming_separator(self) -> str:\r\n+ return self._preset_settings.get('source_naming', {}).get('separator', '_')\r\n+\r\n+ @property\r\n+ def source_naming_indices(self) -> dict:\r\n+ return self._preset_settings.get('source_naming', {}).get('part_indices', {})\r\n+\r\n+ @property\r\n+ def source_glossiness_keywords(self) -> list:\r\n+ return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\r\n+\r\n+ @property\r\n+ def source_bit_depth_variants(self) -> dict:\r\n+ return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\r\n+\r\n+ @property\r\n+ def archetype_rules(self) -> list:\r\n+ return self._preset_settings['archetype_rules']\r\n+\r\n+ @property\r\n+ def asset_category_rules(self) -> dict:\r\n+ return self._preset_settings['asset_category_rules']\r\n+\r\n+ @property\r\n+ def move_to_extra_patterns(self) -> list:\r\n+ return self._preset_settings['move_to_extra_patterns']\r\n+\r\n+ @property\r\n+ def extra_files_subdir(self) -> str:\r\n+ return self._core_settings['EXTRA_FILES_SUBDIR']\r\n+\r\n+ @property\r\n+ def metadata_filename(self) -> str:\r\n+ return self._core_settings['METADATA_FILENAME']\r\n+\r\n+ @property\r\n+ def calculate_stats_resolution(self) -> str:\r\n+ return self._core_settings['CALCULATE_STATS_RESOLUTION']\r\n+\r\n+ @property\r\n+ def map_merge_rules(self) -> list:\r\n+ return self._core_settings['MAP_MERGE_RULES']\r\n+\r\n+ @property\r\n+ def aspect_ratio_decimals(self) -> int:\r\n+ return self._core_settings['ASPECT_RATIO_DECIMALS']\r\n+\r\n+ @property\r\n+ def temp_dir_prefix(self) -> str:\r\n+ return self._core_settings['TEMP_DIR_PREFIX']\r\n+\r\n+ @property\r\n+ def jpg_quality(self) -> int:\r\n+ \"\"\"Gets the configured JPG quality level.\"\"\"\r\n+ return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\r\n+\r\n+ @property\r\n+ def resolution_threshold_for_jpg(self) -> int:\r\n+ \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\r\n+ return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\r\n+\r\n+ @property\r\n+ def respect_variant_map_types(self) -> list:\r\n+ \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\r\n+ # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\r\n+ return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\r\n+\r\n+ @property\r\n+ def force_lossless_map_types(self) -> list:\r\n+ \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\r\n+ return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\r\n+\r\n+ def get_bit_depth_rule(self, map_type: str) -> str:\r\n+ \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\r\n+ rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\r\n+ default_rule = rules.get('DEFAULT', 'respect')\r\n+ return rules.get(map_type, default_rule)\r\n+\r\n+ def get_16bit_output_formats(self) -> tuple[str, str]:\r\n+ \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\r\n+ primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\r\n+ fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\r\n+ return primary.lower(), fallback.lower()\r\n+\r\n+ def get_8bit_output_format(self) -> str:\r\n+ \"\"\"Gets the format name for 8-bit output.\"\"\"\r\n return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower()\n\\ No newline at end of file\n"
}
],
"date": 1745314026627,
"name": "Commit-0",
"content": "# configuration.py\n\nimport json\nimport os\nimport importlib.util\nfrom pathlib import Path\nimport logging\nimport re # Import the regex module\n\nlog = logging.getLogger(__name__) # Use logger defined in main.py\n\n# --- Constants ---\n# Assumes config.py and presets/ are relative to this file's location\nBASE_DIR = Path(__file__).parent\nCORE_CONFIG_PATH = BASE_DIR / \"config.py\"\nPRESETS_DIR = BASE_DIR / \"presets\"\n\n# --- Custom Exception ---\nclass ConfigurationError(Exception):\n \"\"\"Custom exception for configuration loading errors.\"\"\"\n pass\n\n# --- Helper Functions ---\ndef _get_base_map_type(target_map_string: str) -> str:\n \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\n # Use regex to find the leading alphabetical part\n match = re.match(r\"([a-zA-Z]+)\", target_map_string)\n if match:\n return match.group(1).upper()\n # Fallback if no number suffix or unexpected format\n return target_map_string.upper()\n\ndef _fnmatch_to_regex(pattern: str) -> str:\n \"\"\"\n Converts an fnmatch pattern to a regex pattern string.\n Handles basic wildcards (*, ?) and escapes other regex special characters.\n \"\"\"\n i, n = 0, len(pattern)\n res = ''\n while i < n:\n c = pattern[i]\n i = i + 1\n if c == '*':\n res = res + '.*'\n elif c == '?':\n res = res + '.'\n elif c == '[':\n j = i\n if j < n and pattern[j] == '!':\n j = j + 1\n if j < n and pattern[j] == ']':\n j = j + 1\n while j < n and pattern[j] != ']':\n j = j + 1\n if j >= n:\n res = res + '\\\\['\n else:\n stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\n i = j + 1\n if stuff[0] == '!':\n stuff = '^' + stuff[1:]\n elif stuff[0] == '^':\n stuff = '\\\\' + stuff\n res = '%s[%s]' % (res, stuff)\n else:\n res = res + re.escape(c)\n # We want to find the pattern anywhere in the filename for flexibility,\n # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\n # Let's return the core pattern and let the caller decide on anchoring if needed.\n # For filename matching, we usually want to find the pattern, not match the whole string.\n return res\n\n\n# --- Configuration Class ---\nclass Configuration:\n \"\"\"\n Loads and provides access to core settings combined with a specific preset.\n \"\"\"\n def __init__(self, preset_name: str):\n \"\"\"\n Loads core config and the specified preset file.\n\n Args:\n preset_name: The name of the preset (without .json extension).\n\n Raises:\n ConfigurationError: If core config or preset cannot be loaded/validated.\n \"\"\"\n log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\n self.preset_name = preset_name\n self._core_settings: dict = self._load_core_config()\n self._preset_settings: dict = self._load_preset(preset_name)\n self._validate_configs()\n log.debug(f\"Loaded source_naming config: {self._preset_settings.get('source_naming', 'Not Found')}\") # DEBUG: Log loaded source_naming\n self._compile_regex_patterns() # Compile regex after validation\n log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n\n\n def _compile_regex_patterns(self):\n \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\n log.debug(\"Compiling regex patterns from configuration...\")\n self.compiled_extra_regex: list[re.Pattern] = []\n self.compiled_model_regex: list[re.Pattern] = []\n self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\n # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\n self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\n # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\n # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\n\n # Compile Extra Patterns (case-insensitive)\n for pattern in self.move_to_extra_patterns:\n try:\n # Use the raw fnmatch pattern directly if it's simple enough for re.search\n # Or convert using helper if needed. Let's try direct search first.\n # We want to find the pattern *within* the filename.\n regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\n self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\n except re.error as e:\n log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\n\n # Compile Model Patterns (case-insensitive)\n model_patterns = self.asset_category_rules.get('model_patterns', [])\n for pattern in model_patterns:\n try:\n regex_str = _fnmatch_to_regex(pattern)\n self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\n except re.error as e:\n log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\n\n # Compile Bit Depth Variant Patterns (case-sensitive recommended)\n for map_type, pattern in self.source_bit_depth_variants.items():\n try:\n # These often rely on specific suffixes, so anchoring might be better?\n # Let's stick to the converted pattern for now, assuming it ends with suffix.\n regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\n # If the original pattern ended with *, remove the trailing '.*' for suffix matching\n if pattern.endswith('*'):\n regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\n # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\n\n # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\n # This is less strict than anchoring to the end with \\\\.[^.]+$\n final_regex_str = regex_str # Use the result from _fnmatch_to_regex\n self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\n log.debug(f\" Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\n except re.error as e:\n log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\n\n # Compile Map Type Keywords (case-insensitive) based on the new structure\n separator = re.escape(self.source_naming_separator) # Escape separator for regex\n # Use defaultdict to easily append to lists for the same base type\n from collections import defaultdict\n temp_compiled_map_regex = defaultdict(list)\n\n for rule_index, mapping_rule in enumerate(self.map_type_mapping):\n # Validate rule structure (dictionary with target_type and keywords)\n if not isinstance(mapping_rule, dict) or \\\n 'target_type' not in mapping_rule or \\\n 'keywords' not in mapping_rule or \\\n not isinstance(mapping_rule['keywords'], list):\n log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\n continue\n\n target_type = mapping_rule['target_type'].upper() # Use the base type directly\n source_keywords = mapping_rule['keywords']\n\n # Store the rule for potential priority access later (optional, index is now in tuple)\n # self._map_type_rule_order.append(mapping_rule)\n\n if target_type not in self.standard_map_types:\n log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\n # Continue processing anyway, as it might be intended for merging etc.\n\n # Compile keywords for this rule and store with context\n for keyword in source_keywords:\n if not isinstance(keyword, str):\n log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\n continue\n try:\n # Match keyword potentially surrounded by separators or start/end\n # Handle potential wildcards within the keyword using fnmatch conversion\n kw_regex_part = _fnmatch_to_regex(keyword)\n # Build regex to match the keyword part, anchored by separators or string boundaries\n # Use non-capturing groups (?:...)\n # Capture the keyword part itself for potential use later if needed (group 1)\n regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\n compiled_regex = re.compile(regex_str, re.IGNORECASE)\n # Append tuple: (compiled_regex, original_keyword, rule_index)\n temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\n log.debug(f\" Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\n except re.error as e:\n log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\n\n # Assign the compiled regex dictionary\n self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\n log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\n\n log.debug(\"Finished compiling regex patterns.\")\n\n\n def _load_core_config(self) -> dict:\n \"\"\"Loads settings from the core config.py file.\"\"\"\n log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\n if not CORE_CONFIG_PATH.is_file():\n raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\n try:\n spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\n if spec is None or spec.loader is None:\n raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\n core_config_module = importlib.util.module_from_spec(spec)\n # Define default values for core settings in case they are missing in config.py\n default_core_settings = {\n 'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\n 'STANDARD_MAP_TYPES': [],\n 'EXTRA_FILES_SUBDIR': \"Extra\",\n 'METADATA_FILENAME': \"metadata.json\",\n 'IMAGE_RESOLUTIONS': {},\n 'ASPECT_RATIO_DECIMALS': 2,\n 'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\n 'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\n 'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\n 'OUTPUT_FORMAT_8BIT': \"png\",\n 'MAP_MERGE_RULES': [],\n 'CALCULATE_STATS_RESOLUTION': \"1K\",\n 'DEFAULT_ASSET_CATEGORY': \"Texture\",\n 'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\n # --- Additions ---\n 'JPG_QUALITY': 95, # Default JPG quality\n 'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\n 'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\n 'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\n }\n # Load attributes from module, using defaults if missing\n settings = default_core_settings.copy()\n spec.loader.exec_module(core_config_module)\n for name in default_core_settings:\n if hasattr(core_config_module, name):\n settings[name] = getattr(core_config_module, name)\n\n log.debug(f\"Core config loaded successfully.\")\n return settings\n except Exception as e:\n raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\n\n def _load_preset(self, preset_name: str) -> dict:\n \"\"\"Loads the specified preset JSON file.\"\"\"\n log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\n if not PRESETS_DIR.is_dir():\n raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\n\n preset_file = PRESETS_DIR / f\"{preset_name}.json\"\n if not preset_file.is_file():\n raise ConfigurationError(f\"Preset file not found: {preset_file}\")\n\n try:\n with open(preset_file, 'r', encoding='utf-8') as f:\n preset_data = json.load(f)\n log.debug(f\"Preset '{preset_name}' loaded successfully.\")\n return preset_data\n except json.JSONDecodeError as e:\n raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\n except Exception as e:\n raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\n\n def _validate_configs(self):\n \"\"\"Performs basic validation checks on loaded settings.\"\"\"\n log.debug(\"Validating loaded configurations...\")\n # Preset validation\n required_preset_keys = [\n \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\n \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\n ]\n for key in required_preset_keys:\n if key not in self._preset_settings:\n raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\n\n # Validate map_type_mapping structure (new format)\n if not isinstance(self._preset_settings['map_type_mapping'], list):\n raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\n for index, rule in enumerate(self._preset_settings['map_type_mapping']):\n if not isinstance(rule, dict):\n raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\n if 'target_type' not in rule or not isinstance(rule['target_type'], str):\n raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\n if 'keywords' not in rule or not isinstance(rule['keywords'], list):\n raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\n for kw_index, keyword in enumerate(rule['keywords']):\n if not isinstance(keyword, str):\n raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\n\n\n # Core validation (check types or specific values if needed)\n if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\n raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\n if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\n raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\n if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\n raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\n # Add more checks as necessary\n log.debug(\"Configuration validation passed.\")\n\n\n # --- Accessor Methods/Properties ---\n # Use @property for direct access, methods for potentially complex lookups/defaults\n\n @property\n def supplier_name(self) -> str:\n return self._preset_settings.get('supplier_name', 'DefaultSupplier')\n \n @property\n def default_asset_category(self) -> str:\n \"\"\"Gets the default asset category from core settings.\"\"\"\n # Provide a fallback default just in case it's missing from config.py\n return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\n\n @property\n def target_filename_pattern(self) -> str:\n return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\n\n @property\n def image_resolutions(self) -> dict[str, int]:\n return self._core_settings['IMAGE_RESOLUTIONS']\n\n @property\n def standard_map_types(self) -> list[str]:\n return self._core_settings['STANDARD_MAP_TYPES']\n\n @property\n def map_type_mapping(self) -> list:\n return self._preset_settings['map_type_mapping']\n\n @property\n def source_naming_separator(self) -> str:\n return self._preset_settings.get('source_naming', {}).get('separator', '_')\n\n @property\n def source_naming_indices(self) -> dict:\n return self._preset_settings.get('source_naming', {}).get('part_indices', {})\n\n @property\n def source_glossiness_keywords(self) -> list:\n return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\n\n @property\n def source_bit_depth_variants(self) -> dict:\n return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\n\n @property\n def archetype_rules(self) -> list:\n return self._preset_settings['archetype_rules']\n\n @property\n def asset_category_rules(self) -> dict:\n return self._preset_settings['asset_category_rules']\n\n @property\n def move_to_extra_patterns(self) -> list:\n return self._preset_settings['move_to_extra_patterns']\n\n @property\n def extra_files_subdir(self) -> str:\n return self._core_settings['EXTRA_FILES_SUBDIR']\n\n @property\n def metadata_filename(self) -> str:\n return self._core_settings['METADATA_FILENAME']\n\n @property\n def calculate_stats_resolution(self) -> str:\n return self._core_settings['CALCULATE_STATS_RESOLUTION']\n\n @property\n def map_merge_rules(self) -> list:\n return self._core_settings['MAP_MERGE_RULES']\n\n @property\n def aspect_ratio_decimals(self) -> int:\n return self._core_settings['ASPECT_RATIO_DECIMALS']\n\n @property\n def temp_dir_prefix(self) -> str:\n return self._core_settings['TEMP_DIR_PREFIX']\n\n @property\n def jpg_quality(self) -> int:\n \"\"\"Gets the configured JPG quality level.\"\"\"\n return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\n\n @property\n def resolution_threshold_for_jpg(self) -> int:\n \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\n return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\n\n @property\n def respect_variant_map_types(self) -> list:\n \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\n # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\n return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\n\n @property\n def force_lossless_map_types(self) -> list:\n \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\n return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\n\n def get_bit_depth_rule(self, map_type: str) -> str:\n \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\n rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\n default_rule = rules.get('DEFAULT', 'respect')\n return rules.get(map_type, default_rule)\n\n def get_16bit_output_formats(self) -> tuple[str, str]:\n \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\n primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\n fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\n return primary.lower(), fallback.lower()\n\n def get_8bit_output_format(self) -> str:\n \"\"\"Gets the format name for 8-bit output.\"\"\"\n return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower()"
}
]
}