refactor: centralize shared utilities, normalize imports, and configure project linting

This commit is contained in:
Dariusz L
2026-07-16 10:52:06 +02:00
parent 04c3de9533
commit 38dc3e4474
29 changed files with 463 additions and 342 deletions
+2
View File
@@ -36,3 +36,5 @@ tools/aria2/
# User settings – contains API keys and tokens, must NOT be committed
model_resolver_settings.json
node_modules/*
+125 -100
View File
@@ -6,11 +6,11 @@
"""
import asyncio
import threading
from typing import Dict, Any, Optional
import time
import sys
import os
import sys
import threading
import time
from typing import Any, Dict, Optional
if not __package__ or __package__ == "":
this_dir = os.path.dirname(os.path.abspath(__file__))
@@ -25,7 +25,8 @@ if not __package__ or __package__ == "":
sys.modules[package_name] = current_module
if not hasattr(current_module, "__path__"):
current_module.__path__ = [this_dir]
from .core.log_system import LogLevel, create_module_logger, logger as backend_log_controller
from .core.log_system import LogLevel, create_module_logger
from .core.log_system import logger as backend_log_controller
from .core.log_system.config import LOG_LEVEL as BACKEND_DEFAULT_LOG_LEVEL
from .core.path_utils import get_filename_from_path
@@ -99,7 +100,7 @@ else:
MODEL_RESOLVER_DEPENDENCY_NODE_TYPE: MODEL_RESOLVER_DEPENDENCY_NODE_DISPLAY_NAME,
}
__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
class JobProgressTracker:
@@ -140,7 +141,7 @@ class JobProgressTracker:
now = time.time()
with self.lock:
current = self.progress.get(progress_id, {})
# If the job was cancelled, force it to remain cancelled.
if progress_id in self.cancelled and payload.get("status") != "cancelled":
payload["status"] = "cancelled"
@@ -320,130 +321,146 @@ class ModelResolverExtension:
# Import resolver modules
try:
from .core.resolver import (
analyze_and_find_matches,
apply_resolution,
get_local_model_hash_metadata,
invalidate_local_hash_match_cache,
search_local_matches_by_hash,
search_local_matches,
)
from .core.workflow_analyzer import analyze_workflow_models
from .core.path_templates import infer_download_path_templates
from .core.metadata_audit import audit_metadata_sizes
from .core.metadata_builder import (
build_missing_local_metadata,
get_metadata_build_capabilities,
)
from .core.scanner import get_model_files, invalidate_model_files_cache, find_local_file_path
from .core.path_utils import (
extract_safetensors_header_sha256,
get_filename_from_path,
is_path_in_configured_model_roots,
read_json_safe,
write_json_atomic,
)
from .core.network_utils import (
UnsafeUrlError,
host_matches_domain,
request_public_url,
validate_public_http_url,
)
from .core.type_utils import (
first_non_empty,
to_int,
to_bool,
format_size_bytes,
normalize_sha256,
extract_sha256_from_metadata,
fetch_remote_file_size_cached,
looks_like_model_file,
normalize_category_to_model_type,
get_category_folder_keys,
get_enabled_download_categories,
from .core.path_templates import infer_download_path_templates
from .core.path_utils import (
extract_safetensors_header_sha256,
get_filename_from_path,
is_path_in_configured_model_roots,
read_json_safe,
write_json_atomic,
)
from .core.resolver import (
analyze_and_find_matches,
apply_resolution,
get_local_model_hash_metadata,
invalidate_local_hash_match_cache,
search_local_matches,
search_local_matches_by_hash,
)
from .core.scanner import find_local_file_path, get_model_files, invalidate_model_files_cache
from .core.settings import (
TEMPLATE_KEY_ALIASES,
bool_setting as resolver_bool_setting,
get_default_root_for_category,
load_settings as load_resolver_settings,
resolve_download_subfolder,
)
from .core.settings import (
bool_setting as resolver_bool_setting,
)
from .core.settings import (
load_settings as load_resolver_settings,
)
from .core.settings import (
save_settings as save_resolver_settings,
)
from .core.type_utils import (
extract_sha256_from_metadata,
fetch_remote_file_size_cached,
first_non_empty,
format_size_bytes,
get_category_folder_keys,
get_enabled_download_categories,
looks_like_model_file,
normalize_category_to_model_type,
normalize_sha256,
to_bool,
to_int,
)
from .core.workflow_analyzer import analyze_workflow_models
except ImportError as e:
self.logger.error(f"Model Resolver: Could not import core modules: {e}")
return False
# Import download modules
try:
from .core.aria2_installer import Aria2InstallError, install_aria2_engine
from .core.downloader import (
start_background_download,
get_progress,
get_all_progress,
cancel_download,
pause_download,
resume_download,
get_all_progress,
get_aria2_status,
start_aria2_daemon,
stop_aria2_daemon,
get_download_directory,
get_metadata_sidecar_path,
get_progress,
get_safe_metadata_sidecar_path,
normalize_download_category,
write_lora_manager_metadata,
sanitize_download_filename,
is_allowed_model_download_filename,
normalize_download_category,
pause_download,
resume_download,
sanitize_download_filename,
start_aria2_daemon,
start_background_download,
stop_aria2_daemon,
write_lora_manager_metadata,
)
from .core.aria2_installer import Aria2InstallError, install_aria2_engine
from .core.sources.popular import (
get_popular_model_url,
search_popular_models,
reload_databases as reload_popular_databases,
)
from .core.sources.model_list import (
search_model_list,
search_model_list_multiple,
get_model_list_update_status,
update_model_list_from_remote,
reload_model_list,
)
from .core.sources.huggingface import (
search_huggingface_for_file,
parse_huggingface_url,
get_huggingface_download_url,
get_author_fallback_index_status,
refresh_author_fallback_index,
check_huggingface_token,
check_brave_search_api_key,
clear_search_cache as clear_huggingface_search_cache,
)
from .core.sources.civitai import (
search_civitai_for_file,
search_civitai,
parse_civitai_url,
get_civitai_download_url,
get_civitai_model_details,
resolve_urn,
check_civitai_api_key,
check_civitai_session_token,
clear_search_cache as clear_civitai_search_cache,
)
from .core.sources import clear_all_search_caches
from .core.sources.civarchive import (
CivArchiveSearchError,
get_civarchive_model_details,
is_civarchive_available,
search_civarchive_for_file,
parse_civarchive_url,
resolve_civarchive_by_hash,
resolve_civarchive_model_version,
get_civarchive_model_details,
search_civarchive_for_file,
)
from .core.sources.civarchive import (
clear_search_cache as clear_civarchive_search_cache,
)
from .core.sources.civitai import (
check_civitai_api_key,
check_civitai_session_token,
get_civitai_download_url,
get_civitai_model_details,
parse_civitai_url,
resolve_urn,
search_civitai,
search_civitai_for_file,
)
from .core.sources.civitai import (
clear_search_cache as clear_civitai_search_cache,
)
from .core.sources.huggingface import (
check_brave_search_api_key,
check_huggingface_token,
get_author_fallback_index_status,
get_huggingface_download_url,
parse_huggingface_url,
refresh_author_fallback_index,
search_huggingface_for_file,
)
from .core.sources.huggingface import (
clear_search_cache as clear_huggingface_search_cache,
)
from .core.sources.lora_manager_archive import (
clear_search_cache as clear_lora_manager_archive_search_cache,
)
from .core.sources.lora_manager_archive import (
is_lora_manager_archive_available,
search_lora_manager_archive_for_file,
clear_search_cache as clear_lora_manager_archive_search_cache,
)
from .core.sources import clear_all_search_caches
from .core.sources.model_list import (
get_model_list_update_status,
reload_model_list,
search_model_list,
search_model_list_multiple,
update_model_list_from_remote,
)
from .core.sources.popular import (
get_popular_model_url,
search_popular_models,
)
from .core.sources.popular import (
reload_databases as reload_popular_databases,
)
download_available = True
except ImportError as e:
@@ -1139,6 +1156,7 @@ class ModelResolverExtension:
def calculate_sha256_with_progress(normalized_path, progress_id=""):
import os as _os
from .core.path_utils import calculate_file_sha256 as _calculate_file_sha256_core
total_bytes = max(0, _os.path.getsize(normalized_path))
@@ -1618,8 +1636,8 @@ class ModelResolverExtension:
def build_loaded_models_response():
from .core.workflow_analyzer import (
analyze_workflow_models,
URN_TYPE_MAP,
analyze_workflow_models,
)
update_loaded_progress(
@@ -3452,13 +3470,21 @@ class ModelResolverExtension:
if download_available:
from .core.path_utils import (
get_path_abs as get_local_path_abs,
get_path_key as get_local_path_key,
get_path_identity as get_local_path_identity,
get_comfy_root_path,
is_path_within as is_local_path_within,
prefer_local_base_directory,
dedupe_local_base_directories,
get_comfy_root_path,
prefer_local_base_directory,
)
from .core.path_utils import (
get_path_abs as get_local_path_abs,
)
from .core.path_utils import (
get_path_identity as get_local_path_identity,
)
from .core.path_utils import (
get_path_key as get_local_path_key,
)
from .core.path_utils import (
is_path_within as is_local_path_within,
)
# cleanup_search_progress, is_search_progress_cancelled, mark_search_progress_cancelled, and update_search_progress removed
@@ -4084,10 +4110,7 @@ class ModelResolverExtension:
if model_list_result:
confidence = model_list_result.get("confidence", 0)
if is_urn and confidence >= 70:
source_results["model_list"] = model_list_result
source_found = True
elif not is_urn:
if (is_urn and confidence >= 70) or not is_urn:
source_results["model_list"] = model_list_result
source_found = True
@@ -4654,7 +4677,7 @@ class ModelResolverExtension:
if not filename:
# Extract filename from URL
from urllib.parse import urlparse, unquote
from urllib.parse import unquote, urlparse
parsed = urlparse(url)
filename = unquote(parsed.path.split("/")[-1])
@@ -4933,6 +4956,7 @@ class ModelResolverExtension:
async def get_root_directories(request):
"""Get configured ComfyUI root directories for path settings."""
import os
import folder_paths
known_categories = set(folder_paths.folder_names_and_paths.keys())
@@ -5050,6 +5074,7 @@ class ModelResolverExtension:
async def get_subfolders(request):
"""Get known subfolders for a category using ComfyUI folder_paths."""
import os
import folder_paths
raw_category = (request.match_info.get("category") or "").strip()
@@ -5278,7 +5303,7 @@ class ModelResolverExtension:
lines.append(
f"===== {name} ({stat.st_size} bytes, modified {modified_at}) ====="
)
with open(path, "r", encoding="utf-8", errors="replace") as log_file:
with open(path, encoding="utf-8", errors="replace") as log_file:
lines.append(log_file.read().rstrip())
except OSError as exc:
lines.append(f"===== {name} =====")
+2 -3
View File
@@ -11,14 +11,13 @@ Modules:
- sources: Search integrations (HuggingFace, CivitAI, popular models)
"""
from .matcher import find_matches
from .resolver import analyze_and_find_matches, apply_resolution
from .scanner import get_model_files
from .matcher import find_matches
__all__ = [
"analyze_and_find_matches",
"apply_resolution",
"get_model_files",
"find_matches",
"get_model_files",
]
-1
View File
@@ -15,7 +15,6 @@ import requests
from .path_utils import is_path_within
ARIA2_RELEASE_API_URL = "https://api.github.com/repos/aria2/aria2/releases/latest"
REQUEST_HEADERS = {"User-Agent": "ComfyUI-Model-Resolver"}
CUSTOM_NODE_ROOT = Path(__file__).resolve().parents[1]
+2
View File
@@ -1,8 +1,10 @@
"""Unified manager for metadata catalog files (JSON data and metadata sidecars)."""
from typing import Any, Dict
from .path_utils import read_json_safe, save_catalog_with_backup
class CatalogManager:
"""Encapsulates reading, writing, status auditing, and backup logic for catalog files."""
+35 -13
View File
@@ -5,8 +5,6 @@ Handles downloading models from various sources with progress tracking.
"""
import os
import json
import hashlib
import re
import secrets
import shutil
@@ -14,25 +12,44 @@ import socket
import subprocess
import threading
import time
import requests
from typing import Dict, Any, Optional, Callable, List
from pathlib import Path
from collections import deque
from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
import requests
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .resolver import invalidate_local_hash_match_cache, normalize_sha256
from .scanner import invalidate_model_files_cache
from .path_utils import is_path_within, get_path_identity, write_json_atomic, read_json_safe, get_comfy_root_path, calculate_file_sha256, get_filename_from_path
from .network_utils import (
host_matches_domain,
request_public_url,
validate_public_http_url,
)
from .type_utils import as_dict, as_list, first_non_empty, format_size_bytes as format_bytes, DEFAULT_BROWSER_USER_AGENT, extract_response_file_size, normalize_category_to_model_type, get_category_folder_keys, MODEL_EXTENSIONS
from .path_utils import (
calculate_file_sha256,
get_comfy_root_path,
get_filename_from_path,
get_path_identity,
is_path_within,
read_json_safe,
write_json_atomic,
)
from .resolver import invalidate_local_hash_match_cache, normalize_sha256
from .scanner import invalidate_model_files_cache
from .type_utils import (
DEFAULT_BROWSER_USER_AGENT,
MODEL_EXTENSIONS,
as_dict,
as_list,
extract_response_file_size,
first_non_empty,
get_category_folder_keys,
normalize_category_to_model_type,
)
from .type_utils import format_size_bytes as format_bytes
try:
import folder_paths
@@ -72,7 +89,12 @@ HF_XET_ARIA2_AUTH_HOSTS = {
"cas-bridge-direct.xethub-eu.hf.co",
}
from .settings import CATEGORY_MAP, load_settings, normalize_download_backend, normalize_download_category, normalize_relative_subfolder
from .settings import (
load_settings,
normalize_download_backend,
normalize_download_category,
normalize_relative_subfolder,
)
SENSITIVE_METADATA_KEYS = {
"authorization",
@@ -1610,7 +1632,7 @@ def _download_huggingface_xet(
size = os.path.getsize(partial_path)
if expected_size and size != expected_size:
raise IOError(
raise OSError(
f"Downloaded size mismatch: expected {expected_size}, received {size}"
)
os.replace(partial_path, dest_path)
+2 -2
View File
@@ -4,11 +4,11 @@ AzLogs - Central logging system
Provides colored console logging, file rotation, and per-module log levels.
"""
from .logger import logger, LogLevel
from .log_funcs import create_module_logger
from .logger import LogLevel, logger
__all__ = [
"logger",
"LogLevel",
"create_module_logger",
"logger",
]
+3 -4
View File
@@ -4,10 +4,9 @@
@version: 2.0.0
@description: Logging Initializer
"""
# ruff: noqa: T201
import logging
import os
import traceback
import logging
_logger = logging.getLogger(__name__)
_initialized = False
@@ -15,8 +14,8 @@ _project_root = None
_default_module_name = __name__
try:
from .logger import logger, LogLevel, debug, info, warn, error, exception, fatal
from .config import LOG_LEVEL, LOG_MODULE_NAME, USE_COLORS, PROJECT_ALIASES
from .config import LOG_LEVEL, LOG_MODULE_NAME, PROJECT_ALIASES, USE_COLORS
from .logger import LogLevel, debug, error, exception, fatal, info, logger, warn
def _find_project_root(start_path):
current = os.path.dirname(os.path.abspath(start_path))
+7 -8
View File
@@ -11,17 +11,16 @@ Features:
- Log file rotation
- Configuration via environment variables
"""
# ruff: noqa: T201
import os
import sys
import json
import re
import logging
import time
import os
import re
import sys
import threading
import time
import traceback
from enum import IntEnum
from logging.handlers import RotatingFileHandler
import traceback
# Log levels
@@ -546,9 +545,9 @@ class AzLogsLogger:
message = " ".join(str(arg) for arg in args)
# Extract known kwargs
exc_info = kwargs.get("exc_info", None)
exc_info = kwargs.get("exc_info")
stacklevel = kwargs.get("stacklevel", 1)
extra = kwargs.get("extra", None)
extra = kwargs.get("extra")
# Map LogLevel to logging level
log_level = LEVEL_MAP.get(level, logging.INFO)
+15 -14
View File
@@ -4,13 +4,14 @@ Fuzzy Matcher Module
Implements fuzzy string matching to find similar model names.
"""
import heapq
import os
import re
import heapq
from typing import List, Dict, Tuple, Optional, Any
from difflib import SequenceMatcher
from typing import Any, Dict, List, Optional, Tuple
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import get_filename_from_path
@@ -290,6 +291,7 @@ def base_model_score(candidate: str, preferred: Optional[str]) -> float:
# ==================== CENTRALIZED MATCHING & CONFIDENCE HELPERS ====================
from .type_utils import MODEL_EXTENSIONS, PRECISION_FORMAT_SUFFIXES, get_version_sort_key
MODEL_FILE_EXTENSIONS = MODEL_EXTENSIONS
@@ -350,11 +352,11 @@ def calculate_filename_confidence(target_filename: str, candidate_filename: str)
best_similarity = calculate_similarity_with_normalization(
target_filename, candidate_filename
)
# Cap similarity at 0.999 for non-exact matches to prevent false 100% scores
if best_similarity >= 0.999:
best_similarity = 0.999
return round(best_similarity * 100, 1)
@@ -418,26 +420,26 @@ def build_filename_search_queries(filename: str) -> List[str]:
basename = get_filename_from_path(filename).strip()
if not basename:
return []
stem = os.path.splitext(basename)[0].strip()
stem_lower = stem.lower()
# Pattern to match common precision/format suffixes
pattern = r"[-_]?(" + "|".join(PRECISION_FORMAT_SUFFIXES) + r")"
# 1. Strip suffix followed by any other characters (like clean_filename_for_search)
cleaned = re.sub(pattern + r".*$", "", stem, flags=re.IGNORECASE).strip(" -_")
cleaned_lower = cleaned.lower()
# 2. Strip suffix at the end of the stem (like simplified in search queries)
simplified = re.sub(pattern + r"$", "", stem_lower, flags=re.IGNORECASE).strip(" -_")
queries = []
for query in [basename, stem, stem_lower, cleaned, cleaned_lower, simplified]:
query_val = (query or "").strip(" -_")
if query_val and query_val not in queries:
queries.append(query_val)
return queries
@@ -454,8 +456,7 @@ def match_model_by_title_generic(
log_prefix: str = "ModelResolver",
) -> Optional[Dict[str, Any]]:
"""Generic helper to resolve extensionless workflow values by model title across CivitAI and CivArchive."""
from typing import Callable, Any
title_confidence = calculate_model_title_confidence(title_query, model_name)
if title_confidence < MODEL_TITLE_MATCH_THRESHOLD:
log.debug(
@@ -472,7 +473,7 @@ def match_model_by_title_generic(
for version in versions:
hydrated_version = hydrate_version_fn(version) if hydrate_version_fn else version
# Check base model context
if base_model_context:
base_model = get_base_model_fn(hydrated_version)
@@ -492,7 +493,7 @@ def match_model_by_title_generic(
result["confidence"] = title_confidence
result["title_confidence"] = title_confidence
log.info(
f"{log_prefix} model-title match: query={title_query}, model_id={model_id}, model_name={model_name}, version_id={result.get('version_id')}, filename={result.get('filename')}, confidence={title_confidence}, base={result.get('base_model')}"
)
+1 -1
View File
@@ -9,7 +9,6 @@ from urllib.parse import urljoin, urlparse
import requests
REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
SENSITIVE_REDIRECT_HEADERS = {"authorization", "cookie", "proxy-authorization"}
@@ -212,6 +211,7 @@ def request_source_response(
) -> Optional[requests.Response]:
"""Perform a GET/POST request with retry logic for rate limit status (HTTP 429)."""
import time
from .log_system import create_module_logger
log = create_module_logger(__name__)
-1
View File
@@ -12,7 +12,6 @@ from .settings import (
sanitize_folder_name,
)
TEMPLATE_CATEGORIES = (
"loras",
"checkpoints",
+13 -14
View File
@@ -5,12 +5,11 @@ Provides helper functions for path normalization, symlink resolution,
and directory matching to be shared across all modules.
"""
import os
import json
import hashlib
import json
import os
import re
from typing import Any, Tuple, Optional, Callable, Iterable, List, Dict
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
_log = None
@@ -226,13 +225,13 @@ def write_json_atomic(
separators: Optional[Tuple[str, str]] = None,
) -> None:
"""Safely write data to a JSON file using a temporary file and atomic replace."""
import tempfile
import json
import tempfile
dir_name = os.path.dirname(file_path)
if dir_name:
os.makedirs(dir_name, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
prefix=f".{os.path.basename(file_path)}.",
suffix=".tmp",
@@ -550,7 +549,7 @@ def _metadata_get(metadata: Dict[str, Any], *keys: str) -> Any:
if key in metadata:
return metadata.get(key)
lowered = {str(key).lower(): key for key in metadata.keys()}
lowered = {str(key).lower(): key for key in metadata}
for key in keys:
original_key = lowered.get(str(key).lower())
if original_key is not None:
@@ -1001,14 +1000,14 @@ def save_catalog_with_backup(
dir_name = os.path.dirname(data_path)
if dir_name:
os.makedirs(dir_name, exist_ok=True)
# Backup existing data file
if os.path.exists(data_path):
shutil.copy2(data_path, f"{data_path}.bak")
# Write data file atomically
write_json_atomic(data_path, data, indent=indent)
# Write metadata file atomically
write_json_atomic(meta_path, meta, indent=indent)
@@ -1018,7 +1017,7 @@ def read_json_safe(file_path: str, default: Any = None) -> Any:
if not file_path or not os.path.exists(file_path):
return default
try:
with open(file_path, "r", encoding="utf-8") as f:
with open(file_path, encoding="utf-8") as f:
return json.load(f)
except Exception as e:
_get_log().warning(f"Error reading JSON from {os.path.basename(file_path)}: {e}")
@@ -1086,7 +1085,7 @@ def split_path_segments(path_value: Any, filter_dots: bool = True) -> list[str]:
text = str(path_value or "").replace("\\", "/").strip()
if not text:
return []
parts = []
for raw_part in text.split("/"):
part = raw_part.strip()
@@ -1095,6 +1094,6 @@ def split_path_segments(path_value: Any, filter_dots: bool = True) -> list[str]:
if filter_dots and part in {".", ".."}:
continue
parts.append(part)
return parts
+3 -1
View File
@@ -4,8 +4,10 @@ Progress Reporting Module
Unified utility for reporting progress of asynchronous search and download operations.
"""
from typing import Dict, Any, Optional, Callable
from typing import Any, Callable, Dict, Optional
from .log_system import create_module_logger
log = create_module_logger(__name__)
+18 -16
View File
@@ -4,17 +4,29 @@ Core Resolver Module
Integrates all components to provide high-level API for model linking.
"""
import json
import os
import re
import json
import threading
from typing import Dict, Any, List, Optional, Tuple, Callable
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import unquote
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .matcher import find_matches, strip_known_model_extension
from .scanner import get_model_files
from .sources.huggingface import parse_huggingface_url as parse_hf_url
from .type_utils import MODEL_EXTENSIONS as _MODEL_EXTENSIONS
from .type_utils import (
as_dict,
as_list,
extract_sha256_from_metadata,
normalize_sha256,
prepare_remote_size_probe_url,
unique_ordered_strings,
)
from .workflow_analyzer import (
NESTED_MODEL_KEYS,
NODE_TYPE_TO_CATEGORY_HINTS,
@@ -23,11 +35,7 @@ from .workflow_analyzer import (
identify_missing_models,
should_scan_as_model_reference,
)
from .matcher import find_matches, strip_known_model_extension
from .type_utils import as_dict, as_list, MODEL_EXTENSIONS as _MODEL_EXTENSIONS, unique_ordered_strings, extract_sha256_from_metadata, normalize_sha256, prepare_remote_size_probe_url
from .workflow_updater import update_workflow_nodes
from .sources.civitai import resolve_urn
from .sources.huggingface import parse_huggingface_url as parse_hf_url
# Regex patterns for URL extraction (matches HuggingFace and CivitAI URLs)
URL_PATTERN = re.compile(r'(https?://(?:huggingface\.co|civitai\.com)[^\s"\'<>\)\\]+)')
@@ -37,18 +45,12 @@ MODEL_EXTENSIONS = tuple(_MODEL_EXTENSIONS)
from .path_utils import (
get_path_identity,
get_path_abs,
get_path_key,
is_path_within,
prefer_local_base_directory,
dedupe_local_base_directories,
get_filename_from_path,
read_json_safe,
find_metadata_sidecar_path,
get_filename_from_path,
get_path_identity,
read_json_safe,
)
# Imported from .matcher
_LOCAL_HASH_MATCH_CACHE_LOCK = threading.Lock()
@@ -1305,7 +1307,7 @@ def apply_resolution(
}
# If resolved_model provided, extract path if needed
if "resolved_model" in resolution and resolution["resolved_model"]:
if resolution.get("resolved_model"):
resolved_model = resolution["resolved_model"]
if "path" in resolved_model and not mapping.get("resolved_path"):
mapping["resolved_path"] = resolved_model["path"]
+4 -2
View File
@@ -6,13 +6,15 @@ Scans configured model directories and finds available model files.
import os
import time
from typing import List, Dict, Tuple, Optional
from typing import Dict, List, Optional, Tuple
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import get_path_identity, get_filename_from_path
from .path_utils import get_filename_from_path, get_path_identity
# Import folder_paths lazily - it may not be available until ComfyUI is initialized
try:
import folder_paths
+2 -2
View File
@@ -6,12 +6,12 @@ import json
import re
from pathlib import Path
from typing import Any, Dict, Iterable, Mapping, Optional
from .path_utils import write_json_atomic, read_json_safe
from .path_utils import read_json_safe, write_json_atomic
SETTINGS_FILE = Path(__file__).resolve().parents[1] / "model_resolver_settings.json"
from .type_utils import to_bool, CATEGORY_MAP, normalize_download_category
from .type_utils import normalize_download_category, to_bool
DOWNLOAD_PATH_MODES = {"suggested", "template", "manual"}
DOWNLOAD_BACKENDS = {"python", "aria2"}
+38 -30
View File
@@ -4,37 +4,45 @@ Model Sources Module
Provides search functionality for finding models from various sources.
"""
from ..log_system import create_module_logger # noqa: F401
from .popular import search_popular_models, get_popular_model_url
from .model_list import search_model_list, search_model_list_multiple
from .huggingface import (
search_huggingface,
search_huggingface_for_file,
get_huggingface_download_url,
clear_search_cache as clear_huggingface_search_cache,
)
from .civitai import (
search_civitai,
search_civitai_for_file,
get_civitai_download_url,
clear_search_cache as clear_civitai_search_cache,
)
from ..log_system import create_module_logger
from .civarchive import (
clear_search_cache as clear_civarchive_search_cache,
)
from .civarchive import (
is_civarchive_available,
resolve_civarchive_by_hash,
resolve_civarchive_model_version,
search_civarchive,
search_civarchive_for_file,
)
from .civitai import (
clear_search_cache as clear_civitai_search_cache,
)
from .civitai import (
get_civitai_download_url,
search_civitai,
search_civitai_for_file,
)
from .huggingface import (
clear_search_cache as clear_huggingface_search_cache,
)
from .huggingface import (
get_huggingface_download_url,
search_huggingface,
search_huggingface_for_file,
)
from .lora_manager_archive import (
clear_search_cache as clear_lora_manager_archive_search_cache,
)
from .lora_manager_archive import (
get_lora_manager_archive_db_path,
is_lora_manager_archive_available,
search_lora_manager_archive,
search_lora_manager_archive_for_file,
)
from .model_list import search_model_list, search_model_list_multiple
from .popular import get_popular_model_url, search_popular_models
def clear_all_search_caches() -> None:
"""Clear search caches for all external sources."""
@@ -44,26 +52,26 @@ def clear_all_search_caches() -> None:
clear_lora_manager_archive_search_cache()
__all__ = [
"search_popular_models",
"get_popular_model_url",
"search_model_list",
"search_model_list_multiple",
"search_huggingface",
"search_huggingface_for_file",
"get_huggingface_download_url",
"search_civitai",
"search_civitai_for_file",
"get_civitai_download_url",
"clear_all_search_caches",
"clear_civarchive_search_cache",
"clear_lora_manager_archive_search_cache",
"get_civitai_download_url",
"get_huggingface_download_url",
"get_lora_manager_archive_db_path",
"get_popular_model_url",
"is_civarchive_available",
"is_lora_manager_archive_available",
"resolve_civarchive_by_hash",
"resolve_civarchive_model_version",
"search_civarchive",
"search_civarchive_for_file",
"clear_lora_manager_archive_search_cache",
"get_lora_manager_archive_db_path",
"is_lora_manager_archive_available",
"search_civitai",
"search_civitai_for_file",
"search_huggingface",
"search_huggingface_for_file",
"search_lora_manager_archive",
"search_lora_manager_archive_for_file",
"clear_all_search_caches",
"search_model_list",
"search_model_list_multiple",
"search_popular_models",
]
+24 -26
View File
@@ -6,49 +6,47 @@ Search and resolve models from CivArchive.
import html
import json
import os
import re
import time
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import parse_qs, unquote, urljoin, urlparse
from urllib.parse import urljoin, urlparse
import requests
from ..network_utils import host_matches_domain, request_source_response
from ..log_system import create_module_logger
from ..matcher import (
base_model_matches as _base_model_matches,
)
from ..matcher import (
build_filename_search_queries,
calculate_archived_model_confidence,
calculate_model_title_confidence,
has_known_model_extension as _has_known_model_extension,
normalize_base_model as _normalize_base_model,
base_model_matches as _base_model_matches,
base_model_score as _base_model_score,
calculate_candidate_rank,
build_filename_search_queries,
MODEL_TITLE_MATCH_THRESHOLD,
should_update_best_match,
)
from ..matcher import (
has_known_model_extension as _has_known_model_extension,
)
from ..matcher import (
normalize_base_model as _normalize_base_model,
)
from ..network_utils import host_matches_domain, request_source_response
from ..path_utils import get_filename_from_path
from ..progress import get_progress_reporter
from ..type_utils import (
to_int,
CIVARCHIVE_API_TYPE_MAP,
select_primary_model_file,
parse_size_to_bytes,
get_version_sort_key,
DEFAULT_BROWSER_USER_AGENT,
parse_civitai_model_path,
looks_like_model_file,
normalize_model_image,
fetch_remote_file_size_cached,
clear_remote_size_cache,
extract_trained_words,
extract_file_size,
normalize_model_file_info,
build_search_result,
clear_remote_size_cache,
extract_file_size,
extract_trained_words,
fetch_remote_file_size_cached,
looks_like_model_file,
normalize_model_file_info,
normalize_model_image,
normalize_sha256,
prepare_remote_size_probe_url,
select_primary_model_file,
to_int,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
log = create_module_logger(__name__)
+33 -27
View File
@@ -4,40 +4,31 @@ CivitAI Source Module
Search and download models from CivitAI.
"""
import json
import os
import re
import json
import hashlib
import requests
from ..network_utils import request_source_response, request_source_json
from typing import Dict, Any, Optional, List, Callable
from urllib.parse import urlparse, parse_qs, quote, urlencode
from typing import Any, Callable, Dict, List, Optional
from urllib.parse import quote, urlencode
import requests # noqa: F401 – used indirectly via unittest.mock patch targets
from ..log_system import create_module_logger
from ..matcher import (
calculate_filename_confidence,
calculate_model_title_confidence,
has_known_model_extension as _has_known_model_extension,
normalize_base_model as _normalize_base_model,
base_model_matches as _base_model_matches,
)
from ..matcher import (
base_model_score as _base_model_score,
)
from ..type_utils import (
first_non_empty,
CIVITAI_API_TYPE_MAP,
select_primary_model_file,
parse_size_to_bytes,
get_version_sort_key,
check_credential_http,
DEFAULT_BROWSER_USER_AGENT,
parse_civitai_model_path,
normalize_model_image,
as_dict,
as_list,
extract_trained_words,
build_search_result,
normalize_model_file_info,
from ..matcher import (
calculate_filename_confidence,
)
from ..progress import report_progress, get_progress_reporter
from ..matcher import (
has_known_model_extension as _has_known_model_extension,
)
from ..matcher import (
normalize_base_model as _normalize_base_model,
)
from ..network_utils import request_source_json, request_source_response
from ..path_utils import (
calculate_file_sha256,
extract_safetensors_header_metadata,
@@ -46,7 +37,22 @@ from ..path_utils import (
infer_safetensors_base_model,
read_json_safe,
)
from ..log_system import create_module_logger
from ..progress import get_progress_reporter
from ..type_utils import (
CIVITAI_API_TYPE_MAP,
DEFAULT_BROWSER_USER_AGENT,
as_dict,
as_list,
build_search_result,
check_credential_http,
extract_trained_words,
first_non_empty,
normalize_model_file_info,
normalize_model_image,
parse_size_to_bytes,
select_primary_model_file,
)
log = create_module_logger(__name__)
+14 -9
View File
@@ -4,23 +4,28 @@ HuggingFace Source Module
Search and download models from HuggingFace Hub.
"""
import json
import os
import re
import threading
import time
import requests
from typing import Dict, Any, Optional, List, Callable
from urllib.parse import urlparse, quote
from typing import Any, Callable, Dict, List, Optional
from urllib.parse import quote, urlparse
from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
from ..progress import get_progress_reporter
log = create_module_logger(__name__)
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe, get_filename_from_path
from ..network_utils import host_matches_domain, request_source_response, request_source_json
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size, prepare_remote_size_probe_url
from ..matcher import build_filename_search_queries, clean_filename_for_search
from ..matcher import build_filename_search_queries
from ..network_utils import host_matches_domain, request_source_json, request_source_response
from ..path_utils import METADATA_DIR, get_filename_from_path, read_json_safe, write_json_atomic
from ..type_utils import (
check_credential_http,
clear_remote_size_cache,
extract_file_size,
fetch_remote_file_size_cached,
prepare_remote_size_probe_url,
)
HF_API_URL = "https://huggingface.co/api"
HF_AUTHOR_FALLBACKS = ["Comfy-Org"]
+15 -8
View File
@@ -6,28 +6,35 @@ Search archived CivitAI metadata stored by comfyui-lora-manager.
import json
import os
import sqlite3
import re
import sqlite3
import time
from typing import Any, Callable, Dict, List, Optional
from ..log_system import create_module_logger
from ..matcher import (
base_model_matches as _base_model_matches,
)
from ..matcher import (
base_model_score as _base_model_score,
)
from ..matcher import (
calculate_archived_model_confidence,
normalize_base_model as _normalize_base_model,
base_model_matches as _base_model_matches,
base_model_score as _base_model_score,
calculate_candidate_rank,
should_update_best_match,
)
from ..matcher import (
normalize_base_model as _normalize_base_model,
)
from ..path_utils import get_filename_from_path
from ..progress import get_progress_reporter
from ..type_utils import (
select_primary_model_file,
build_search_result,
get_generic_filename_tokens,
normalize_lora_manager_type,
build_search_result,
select_primary_model_file,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
log = create_module_logger(__name__)
+8 -9
View File
@@ -4,22 +4,20 @@ Model List Database Module
Search the ComfyUI Manager model-list.json database with fuzzy matching.
"""
import os
import hashlib
import shutil
import tempfile
import os
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List
import requests
from ..matcher import normalize_filename, calculate_similarity
from typing import Any, Dict, List, Optional
from ..log_system import create_module_logger
from ..matcher import calculate_similarity, normalize_filename
log = create_module_logger(__name__)
from ..path_utils import METADATA_DIR
from ..catalog_manager import CatalogManager
from ..path_utils import METADATA_DIR
MODEL_LIST_FILE = os.path.join(METADATA_DIR, "model-list.json")
MODEL_LIST_META_FILE = os.path.join(METADATA_DIR, "model-list.meta.json")
catalog_mgr = CatalogManager(MODEL_LIST_FILE, MODEL_LIST_META_FILE, "models")
@@ -30,6 +28,7 @@ MODEL_LIST_GITHUB_API_URL = (
"https://api.github.com/repos/Comfy-Org/ComfyUI-Manager/contents/model-list.json?ref=main"
)
from ..network_utils import request_source_json
HTTP_TIMEOUT = 30
# Cache for loaded data
@@ -69,7 +68,7 @@ def _get_local_model_list_sha() -> str:
try:
with open(MODEL_LIST_FILE, "rb") as f:
content = f.read()
header = f"blob {len(content)}\0".encode("utf-8")
header = f"blob {len(content)}\0".encode()
return hashlib.sha1(header + content).hexdigest()
except Exception as e:
log.warning(f"Error calculating model-list.json SHA: {e}")
+27 -29
View File
@@ -5,20 +5,19 @@ Curated list of common models with known download URLs.
"""
import os
import json
import re
import shutil
import tempfile
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List
from typing import Any, Dict, List, Optional
from ..log_system import create_module_logger
log = create_module_logger(__name__)
from ..path_utils import METADATA_DIR, read_json_safe
from ..network_utils import request_source_json
from ..catalog_manager import CatalogManager
from ..network_utils import request_source_json
from ..path_utils import METADATA_DIR, read_json_safe
POPULAR_MODELS_FILE = os.path.join(METADATA_DIR, "popular-models.json")
MODEL_ALIASES_FILE = os.path.join(METADATA_DIR, "model-aliases.json")
BASE_MODELS_FILE = os.path.join(METADATA_DIR, "base-models.json")
@@ -56,7 +55,6 @@ def _load_model_aliases() -> Dict[str, List[str]]:
from ..matcher import normalize_base_model as _normalize_base_model
base_models_mgr = CatalogManager(BASE_MODELS_FILE, BASE_MODELS_META_FILE, "base_models")
@@ -212,19 +210,19 @@ def _read_base_models_meta() -> Dict[str, Any]:
def generate_aliases(name: str) -> List[str]:
aliases = {name.lower()}
# Replace dots, dashes, underscores with spaces, then normalize spaces
normalized = re.sub(r"[\._\-]+", " ", name.lower())
normalized = re.sub(r"\s+", " ", normalized).strip()
aliases.add(normalized)
version_trimmed = re.sub(r"(\d+)(?:[\s\._\-]+0)+(?!\d)", r"\1", normalized)
aliases.add(version_trimmed)
# Remove all spaces/dashes/dots/underscores
collapsed = re.sub(r"[\s\._\-]+", "", name.lower())
aliases.add(collapsed)
aliases.add(re.sub(r"[\s\._\-]+", "", version_trimmed))
# If there are dots/dashes/underscores, add specific clean replacements
if "." in name:
aliases.add(name.lower().replace(".", ""))
@@ -242,7 +240,7 @@ def generate_aliases(name: str) -> List[str]:
first_word = words[0]
if len(first_word) > 2 and first_word not in {"stable", "flux", "pony", "sdxl"}:
aliases.add(first_word)
return sorted(list({a for a in aliases if len(a) > 1}))
@@ -251,45 +249,45 @@ def get_base_models_status(check_remote: bool = False) -> Dict[str, Any]:
local_data = _read_base_models_file()
base_models = local_data.get("base_models", [])
meta = _read_base_models_meta()
local_count = len(base_models)
local_updated_at = meta.get("updated_at") or ""
status = {
"local_count": local_count,
"local_updated_at": local_updated_at,
"remote_checked_at": meta.get("last_checked_at") or "",
"update_available": False,
}
if check_remote:
try:
# Fetch from CivitAI API using unified helper
enums = request_source_json("https://civitai.com/api/v1/enums", timeout=15, log_name="CivitAI Enums")
if enums:
remote_models = enums.get("BaseModel", [])
# Check if there are new models by comparing normalized names and aliases
all_existing_normalized = set()
for m in base_models:
all_existing_normalized.add(_normalize_base_model(m.get("name", "")))
for alias in m.get("aliases", []):
all_existing_normalized.add(_normalize_base_model(alias))
new_models_found = False
for remote_name in remote_models:
norm_remote = _normalize_base_model(remote_name)
if norm_remote and norm_remote not in all_existing_normalized:
new_models_found = True
break
status.update({
"remote_checked_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
"update_available": new_models_found
})
except Exception as e:
log.warning(f"Error checking remote base models: {e}")
return status
@@ -301,33 +299,33 @@ def update_base_models_from_remote() -> Dict[str, Any]:
remote_names = enums.get("BaseModel", [])
if not remote_names or not isinstance(remote_names, list):
raise ValueError("CivitAI enums did not return a valid BaseModel list")
local_data = _read_base_models_file()
base_models = local_data.get("base_models", [])
# 1. Build a set of all normalized aliases currently in base-models.json
all_known_normalized = set()
for m in base_models:
for alias in m.get("aliases", []):
all_known_normalized.add(_normalize_base_model(alias))
all_known_normalized.add(_normalize_base_model(m.get("name", "")))
updated_models = list(base_models)
new_added_count = 0
new_added_names = []
for name in remote_names:
norm_name = _normalize_base_model(name)
if not norm_name:
continue
# If the normalized name is already a known alias or name, skip it
if norm_name in all_known_normalized:
continue
# Generate candidate aliases
candidates = generate_aliases(name)
# Filter out aliases that are already registered/known
filtered_aliases = []
for alias in candidates:
@@ -335,20 +333,20 @@ def update_base_models_from_remote() -> Dict[str, Any]:
if norm_alias not in all_known_normalized:
filtered_aliases.append(alias)
all_known_normalized.add(norm_alias)
# If we have valid aliases, add the new base model entry
if filtered_aliases:
if name.lower() not in [a.lower() for a in filtered_aliases]:
filtered_aliases.insert(0, name)
all_known_normalized.add(norm_name)
updated_models.append({
"name": name,
"aliases": filtered_aliases
})
new_added_count += 1
new_added_names.append(name)
now_str = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
meta = {
"updated_at": now_str,
@@ -359,7 +357,7 @@ def update_base_models_from_remote() -> Dict[str, Any]:
}
base_models_mgr.save({"base_models": updated_models}, meta, indent=2)
reload_databases()
return {
"local_count": len(updated_models),
"local_updated_at": now_str,
+13 -11
View File
@@ -262,8 +262,9 @@ CIVARCHIVE_API_TYPE_MAP = {
"motion_modules": "MotionModule",
}
from typing import Any, Dict, List, Optional, Tuple, Callable
import re
from typing import Any, Callable, Dict, List, Optional, Tuple
def normalize_download_category(category: str) -> str:
"""Return the canonical ComfyUI folder_paths key for a download category."""
@@ -561,7 +562,7 @@ def fetch_remote_file_size(
"""
from .network_utils import request_public_url
request_headers = {**(headers or {}), "Accept-Encoding": "identity"}
size = None
try:
response, _final_url, _final_headers = request_public_url(
@@ -604,6 +605,7 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
"""
import os
from urllib.parse import unquote, urlparse
from .network_utils import host_matches_domain
from .path_utils import get_filename_from_path
@@ -620,7 +622,7 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
if host_matches_domain(host, "huggingface.co") and path.startswith("/spaces/"):
return False
# Civitai/CivArchive api/download url prefix check
if (
host_matches_domain(host, "civitai.com")
@@ -650,7 +652,7 @@ def normalize_model_image(image_data: Dict[str, Any], default_civitai_url: str =
meta = {}
url = image_data.get("url") or image_data.get("imageUrl") or image_data.get("src") or ""
civitai_url = image_data.get("civitaiUrl") or image_data.get("postUrl") or default_civitai_url
if not civitai_url and isinstance(image_data.get("id"), (int, str)):
civitai_url = f"https://civitai.com/images/{image_data.get('id')}"
@@ -693,7 +695,6 @@ def normalize_model_image(image_data: Dict[str, Any], default_civitai_url: str =
}
import re
SHA256_PATTERN = re.compile(r"^[a-fA-F0-9]{64}$")
@@ -836,6 +837,7 @@ def prepare_remote_size_probe_url(url: Any) -> Optional[str]:
Converts HuggingFace '/blob/' URLs to '/resolve/' URLs.
"""
from urllib.parse import urlparse
from .network_utils import host_matches_domain
if not isinstance(url, str) or not url.strip():
@@ -992,7 +994,7 @@ def get_enabled_download_categories(folder_names: list[str]) -> list[str]:
"text_encoders", "ipadapter", "sams", "ultralytics"
]
skip = {"custom_nodes", "configs"}
categories = []
for cat in [*preferred, *folder_names]:
norm = normalize_download_category(cat)
@@ -1105,12 +1107,12 @@ def build_search_result(
def parse_provider_model_url(url: str, allowed_domains: list[str]) -> Optional[Dict[str, Any]]:
"""Parse a provider model URL (e.g. CivitAI or mirror site) to extract model/version info."""
from urllib.parse import urlparse
import re
from urllib.parse import urlparse
if not isinstance(url, str) or not url.strip():
return None
parsed = urlparse(url)
hostname = parsed.hostname
if hostname:
@@ -1122,7 +1124,7 @@ def parse_provider_model_url(url: str, allowed_domains: list[str]) -> Optional[D
break
if not matched:
return None
# Standard CivArchive sha256 mirrors pattern
sha_match = re.search(r"/sha256/([a-fA-F0-9]{64})", parsed.path)
if sha_match:
@@ -1133,6 +1135,6 @@ def parse_provider_model_url(url: str, allowed_domains: list[str]) -> Optional[D
match = re.search(r"/api/download/models/(\d+)", parsed.path)
if match:
return {"version_id": int(match.group(1))}
return parse_civitai_model_path(parsed.path, parsed.query)
+7 -6
View File
@@ -7,9 +7,10 @@ Extracts model references from workflow JSON and identifies missing models.
import os
import re
import threading
from typing import List, Dict, Any, Optional, Set, Callable
from typing import Any, Callable, Dict, List, Optional, Set
from .log_system import create_module_logger
log = create_module_logger(__name__)
@@ -21,7 +22,7 @@ except ImportError:
log.warning("Model Resolver: folder_paths not available yet - will retry later")
from .type_utils import URN_TYPE_MAP, MODEL_EXTENSIONS, unique_ordered_strings
from .type_utils import MODEL_EXTENSIONS, URN_TYPE_MAP, unique_ordered_strings
URN_REGEX = re.compile(r"^urn:air:([^:]+):([^:]+):([^:]+):(\d+)@(\d+)$")
@@ -734,7 +735,7 @@ def _build_dynamic_node_widget_category_hints(node_type: str) -> Dict[str, Any]:
try:
if folder_paths_module is not None and callable(get_filename_list):
setattr(folder_paths_module, "get_filename_list", traced_get_filename_list)
folder_paths_module.get_filename_list = traced_get_filename_list
patched_folder_paths = True
patch_get_filename_list_global(input_types_func)
patch_get_filename_list_global(schema_func)
@@ -750,7 +751,7 @@ def _build_dynamic_node_widget_category_hints(node_type: str) -> Dict[str, Any]:
for global_scope, global_name, global_value in patched_globals:
global_scope[global_name] = global_value
if patched_folder_paths:
setattr(folder_paths_module, "get_filename_list", get_filename_list)
folder_paths_module.get_filename_list = get_filename_list
if not isinstance(input_types, dict) and schema is None:
return empty_hints
@@ -1037,7 +1038,7 @@ def get_model_widget_category_hints(
if indexed_category_hint:
hints_list.append(indexed_category_hint)
# If the widget category hint is just "diffusion_models", and we have a more specific output hint,
# let the output hint take priority.
if widget_category_hint == "diffusion_models" and output_widget_category_hint and output_widget_category_hint != "diffusion_models":
@@ -1048,7 +1049,7 @@ def get_model_widget_category_hints(
hints_list.append(widget_category_hint)
if output_widget_category_hint:
hints_list.append(output_widget_category_hint)
return _ordered_unique_categories(hints_list)
+3 -3
View File
@@ -5,12 +5,12 @@ Updates workflow JSON by replacing model paths in nodes.
"""
import os
from typing import Dict, Any, List, Optional
from typing import Any, Dict, List, Optional
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import is_path_within, get_filename_from_path
log = create_module_logger(__name__)
from .path_utils import get_filename_from_path, is_path_within
def convert_to_relative_path(
+46
View File
@@ -19,3 +19,49 @@ PublisherId = "azornes"
DisplayName = "Comfyui Model Resolver"
Icon = ""
requires-comfyui = ">=0.26.0"
# ---------------------------------------------------------------------------
# Ruff – static analysis for Python
# ---------------------------------------------------------------------------
[tool.ruff]
target-version = "py310"
line-length = 120
[tool.ruff.lint]
# Rule sets:
# E/W – pycodestyle errors & warnings
# F – pyflakes
# I – isort (import sorting)
# B – flake8-bugbear
# SIM – flake8-simplify
# UP – pyupgrade
# RUF – Ruff-specific rules
select = ["E", "W", "F", "I", "B", "SIM", "UP", "RUF"]
ignore = [
"E501", # line-too-long – handled by formatter / not enforced
"E402", # module-import-not-at-top – ComfyUI entry point needs late imports
"W293", # blank-line-with-whitespace – CRLF artifacts on indented blank lines
"B023", # function-uses-loop-variable – intentional in callback patterns
"SIM105", # suppressible-exception – contextlib.suppress not always clearer
"SIM108", # if-else-block-instead-of-if-exp – readability preference
"UP006", # non-pep585-annotation – project targets >=3.10 but uses typing compat
"UP035", # deprecated-import – typing compat kept for clarity
"RUF013", # implicit-optional – existing API signatures
"UP045", # non-pep604-annotation-optional – existing code uses Optional[X]
"UP038", # non-pep604-isinstance – existing isinstance(x, (A, B)) patterns
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"F401", # unused-import – re-exports
"E402", # module-import-not-at-top – ComfyUI entry point structure
"F811", # redefined-while-unused – re-exports / conditional imports
]
"tests/*" = [
"B011", # assert-false – ok in tests
"SIM300", # yoda-conditions – ok in tests
]
[tool.ruff.lint.isort]
known-first-party = ["core"]
+1 -2
View File
@@ -8,10 +8,9 @@ from core.sources.huggingface import (
parse_huggingface_url,
get_huggingface_download_url,
_normalize_huggingface_size_probe_url,
clean_filename_for_search,
_write_persistent_author_index,
)
from core.matcher import build_filename_search_queries
from core.matcher import build_filename_search_queries, clean_filename_for_search
from core.type_utils import extract_file_size
class HuggingFaceSourceTests(unittest.TestCase):