refactor: complete migration to centralized HTTP helpers and shared normalization
- Replace remaining direct requests.get calls with request_public_url, request_source_response, and request_source_json in downloader, civarchive, civitai, and popular sources - Use normalize_model_file_info in CivitAI source instead of inline logic - Update huggingface-author-index.json metadata - Add tests for civarchive page/search requests and popular base model updates - Adjust downloader redirect test for new request_public_url behavior
This commit is contained in:
+5
-18
@@ -228,34 +228,21 @@ def _resolve_civitai_download_url_for_aria2(
|
||||
if not _is_civitai_api_download_url(validated_url):
|
||||
return validated_url
|
||||
|
||||
response = None
|
||||
try:
|
||||
response = requests.get(
|
||||
response, resolved_url, _ = request_public_url(
|
||||
"GET",
|
||||
validated_url,
|
||||
headers=build_download_headers(validated_url, headers),
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
timeout=20,
|
||||
stream=True,
|
||||
max_redirects=1,
|
||||
)
|
||||
if response.status_code not in {301, 302, 303, 307, 308}:
|
||||
return url
|
||||
|
||||
location = response.headers.get("Location") or response.headers.get("location")
|
||||
if not location:
|
||||
return url
|
||||
|
||||
resolved_url = validate_public_http_url(urljoin(validated_url, location.strip()))
|
||||
response.close()
|
||||
log.debug("Resolved CivitAI download redirect for aria2")
|
||||
return resolved_url
|
||||
except Exception as exc:
|
||||
log.warning(f"Could not pre-resolve CivitAI download URL for aria2: {exc}")
|
||||
return validated_url
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _resolve_download_url_for_aria2(
|
||||
|
||||
+20
-21
@@ -14,7 +14,7 @@ from urllib.parse import parse_qs, unquote, urljoin, urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from ..network_utils import host_matches_domain
|
||||
from ..network_utils import host_matches_domain, request_source_response
|
||||
from ..matcher import (
|
||||
calculate_archived_model_confidence,
|
||||
calculate_model_title_confidence,
|
||||
@@ -155,16 +155,15 @@ def _request_page_text(
|
||||
timeout: int = 20,
|
||||
) -> Optional[str]:
|
||||
url = urljoin(CIVARCHIVE_BASE_URL, path_or_url)
|
||||
request_params = {k: v for k, v in (params or {}).items() if v is not None}
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
params=request_params,
|
||||
headers=REQUEST_HEADERS,
|
||||
timeout=timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"CivArchive page request failed: url={url}, error={e}")
|
||||
response = request_source_response(
|
||||
url,
|
||||
method="GET",
|
||||
headers=REQUEST_HEADERS,
|
||||
params=params,
|
||||
timeout=timeout,
|
||||
log_name="CivArchive page",
|
||||
)
|
||||
if response is None:
|
||||
return None
|
||||
|
||||
if response.status_code != 200:
|
||||
@@ -417,16 +416,16 @@ def _search_page(
|
||||
# (especially VAEs) are frequently misclassified (e.g. as Checkpoint or null type) in their database.
|
||||
# We rely on name confidence sorting instead.
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{CIVARCHIVE_BASE_URL}/search",
|
||||
params=params,
|
||||
headers=REQUEST_HEADERS,
|
||||
timeout=timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"CivArchive search request failed: query={query}, error={e}")
|
||||
raise CivArchiveSearchError(str(e)) from e
|
||||
response = request_source_response(
|
||||
f"{CIVARCHIVE_BASE_URL}/search",
|
||||
method="GET",
|
||||
headers=REQUEST_HEADERS,
|
||||
params=params,
|
||||
timeout=timeout,
|
||||
log_name="CivArchive search",
|
||||
)
|
||||
if response is None:
|
||||
raise CivArchiveSearchError("CivArchive search request failed (network error or timeout)")
|
||||
|
||||
if response.status_code != 200:
|
||||
log.warning(
|
||||
|
||||
+8
-16
@@ -35,6 +35,7 @@ from ..type_utils import (
|
||||
as_list,
|
||||
extract_trained_words,
|
||||
build_search_result,
|
||||
normalize_model_file_info,
|
||||
)
|
||||
from ..progress import report_progress, get_progress_reporter
|
||||
from ..path_utils import (
|
||||
@@ -1129,22 +1130,13 @@ def _normalize_civitai_file(
|
||||
version_id: Optional[int],
|
||||
api_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
hashes = file_info.get("hashes") if isinstance(file_info.get("hashes"), dict) else {}
|
||||
return {
|
||||
"id": file_info.get("id"),
|
||||
"name": file_info.get("name") or file_info.get("filename"),
|
||||
"type": file_info.get("type"),
|
||||
"size": file_info.get("sizeKB", 0) * 1024 if file_info.get("sizeKB") else file_info.get("size"),
|
||||
"download_url": file_info.get("downloadUrl") or (
|
||||
get_civitai_download_url(version_id, api_key) if version_id else None
|
||||
),
|
||||
"primary": bool(file_info.get("primary", False)),
|
||||
"sha256": file_info.get("sha256") or hashes.get("SHA256") or hashes.get("sha256"),
|
||||
"hashes": hashes,
|
||||
"metadata": file_info.get("metadata") or {},
|
||||
"model_id": model_id,
|
||||
"version_id": version_id,
|
||||
}
|
||||
fallback_url = get_civitai_download_url(version_id, api_key) if version_id else None
|
||||
return normalize_model_file_info(
|
||||
file_info,
|
||||
model_id=model_id,
|
||||
version_id=version_id,
|
||||
fallback_download_url=fallback_url
|
||||
)
|
||||
|
||||
|
||||
def get_civitai_model_details(
|
||||
|
||||
+7
-11
@@ -17,6 +17,7 @@ log = create_module_logger(__name__)
|
||||
|
||||
|
||||
from ..path_utils import METADATA_DIR, read_json_safe, write_json_atomic, save_catalog_with_backup
|
||||
from ..network_utils import request_source_json
|
||||
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")
|
||||
@@ -259,11 +260,9 @@ def get_base_models_status(check_remote: bool = False) -> Dict[str, Any]:
|
||||
|
||||
if check_remote:
|
||||
try:
|
||||
import requests
|
||||
# Fetch from CivitAI API
|
||||
response = requests.get("https://civitai.com/api/v1/enums", timeout=15)
|
||||
if response.status_code == 200:
|
||||
enums = response.json()
|
||||
# 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
|
||||
@@ -292,12 +291,9 @@ def get_base_models_status(check_remote: bool = False) -> Dict[str, Any]:
|
||||
|
||||
def update_base_models_from_remote() -> Dict[str, Any]:
|
||||
"""Fetch live BaseModel enums from CivitAI and merge into base-models.json."""
|
||||
import requests
|
||||
response = requests.get("https://civitai.com/api/v1/enums", timeout=15)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"CivitAI enums endpoint returned HTTP {response.status_code}")
|
||||
|
||||
enums = response.json()
|
||||
enums = request_source_json("https://civitai.com/api/v1/enums", timeout=15, log_name="CivitAI Enums", raise_on_error=True)
|
||||
if not enums:
|
||||
raise ValueError("CivitAI enums did not return a valid response")
|
||||
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")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -91,6 +91,73 @@ class TestRefactoringUnification(unittest.TestCase):
|
||||
res = search_civitai_by_hash("fake_hash")
|
||||
self.assertIsNone(res)
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_page_text_success(self, mock_get):
|
||||
from core.sources.civarchive import _request_page_text
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = "<html>content</html>"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
res = _request_page_text("test_path")
|
||||
self.assertEqual(res, "<html>content</html>")
|
||||
mock_get.assert_called_once()
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_page_text_fail(self, mock_get):
|
||||
from core.sources.civarchive import _request_page_text
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
res = _request_page_text("test_path")
|
||||
self.assertIsNone(res)
|
||||
|
||||
mock_get.side_effect = Exception("network error")
|
||||
res2 = _request_page_text("test_path")
|
||||
self.assertIsNone(res2)
|
||||
|
||||
@patch("requests.get")
|
||||
def test_get_base_models_status_remote(self, mock_get):
|
||||
from core.sources.popular import get_base_models_status
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"BaseModel": ["SD 1.5", "SDXL 1.0"]}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
res = get_base_models_status(check_remote=True)
|
||||
self.assertIn("local_count", res)
|
||||
self.assertIn("update_available", res)
|
||||
mock_get.assert_called_once_with("https://civitai.com/api/v1/enums", params={}, headers=None, timeout=15)
|
||||
|
||||
@patch("core.sources.popular.save_catalog_with_backup")
|
||||
@patch("core.sources.popular.read_json_safe")
|
||||
@patch("requests.get")
|
||||
def test_update_base_models_from_remote_success(self, mock_get, mock_read_json, mock_save_catalog):
|
||||
from core.sources.popular import update_base_models_from_remote
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"BaseModel": ["SD 1.5", "SDXL 1.0", "NewModel"]}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Mock reading base-models.json
|
||||
mock_read_json.return_value = {
|
||||
"base_models": [
|
||||
{"name": "SD 1.5", "aliases": ["sd1.5"]},
|
||||
{"name": "SDXL 1.0", "aliases": ["sdxl"]},
|
||||
]
|
||||
}
|
||||
|
||||
res = update_base_models_from_remote()
|
||||
self.assertTrue(res.get("updated"))
|
||||
self.assertEqual(res.get("new_models_added"), 1)
|
||||
self.assertIn("NewModel", res.get("new_models_added_list", []))
|
||||
|
||||
# Verify it saved the updated catalog
|
||||
mock_save_catalog.assert_called_once()
|
||||
saved_data = mock_save_catalog.call_args[0][1]
|
||||
self.assertEqual(len(saved_data["base_models"]), 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -314,17 +314,23 @@ class ModelResolverRobustnessTests(unittest.TestCase):
|
||||
api_url = "https://civitai.com/api/download/models/123"
|
||||
redirect_url = "https://civitai-delivery.net/models/123/model.safetensors?token=abc"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 302
|
||||
mock_response.headers = {"Location": redirect_url}
|
||||
mock_response1 = MagicMock()
|
||||
mock_response1.status_code = 302
|
||||
mock_response1.headers = {"Location": redirect_url}
|
||||
|
||||
with patch("requests.get", return_value=mock_response) as mock_get, patch(
|
||||
mock_response2 = MagicMock()
|
||||
mock_response2.status_code = 200
|
||||
|
||||
with patch("requests.get", side_effect=[mock_response1, mock_response2]) as mock_get, patch(
|
||||
"core.downloader.validate_public_http_url",
|
||||
side_effect=lambda value: value,
|
||||
), patch(
|
||||
"core.network_utils.validate_public_http_url",
|
||||
side_effect=lambda value: value,
|
||||
):
|
||||
resolved = _resolve_civitai_download_url_for_aria2(api_url)
|
||||
self.assertEqual(resolved, redirect_url)
|
||||
mock_get.assert_called_once()
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
# Scenario 2: Non-CivitAI URL (e.g. HuggingFace) should bypass redirects immediately
|
||||
hf_url = "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/model.safetensors"
|
||||
|
||||
Reference in New Issue
Block a user