Replace the 1095-line monolithic SearchService with a thin facade that delegates to SearchOrchestrator. Split the implementation into three focused modules: - search_orchestrator.py: request parsing, provider coordination, and response construction - search_providers.py: per-source execution, progress callbacks, and async task creation - search_cache.py: timestamping and stable cache stamping for results Add unit tests for the new components in tests/test_search_service_components.py. Preserve the existing HTTP route behavior by keeping SearchService.search_sources as the stable route-facing API.
105 lines
3.0 KiB
Python
105 lines
3.0 KiB
Python
"""Timestamping and cache helpers for source search results."""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
SEARCH_RESULT_KEYS = (
|
|
"popular",
|
|
"model_list",
|
|
"huggingface",
|
|
"civitai",
|
|
"civarchive",
|
|
"lora_manager_archive",
|
|
)
|
|
|
|
|
|
class SearchResultCache:
|
|
"""Keep stable timestamps for equivalent search results."""
|
|
|
|
def __init__(self, timestamps: dict[str, str]):
|
|
self._timestamps = timestamps
|
|
|
|
@staticmethod
|
|
def _current_timestamp() -> str:
|
|
return (
|
|
datetime.now(timezone.utc)
|
|
.replace(microsecond=0)
|
|
.isoformat()
|
|
)
|
|
|
|
@staticmethod
|
|
def _result_signature(source_key: str, result: Any) -> str:
|
|
if isinstance(result, list):
|
|
return "|".join(
|
|
SearchResultCache._result_signature(source_key, item)
|
|
for item in result
|
|
)
|
|
if not isinstance(result, dict):
|
|
return ""
|
|
|
|
parts = [
|
|
source_key,
|
|
result.get("download_url")
|
|
or result.get("url")
|
|
or result.get("model_url")
|
|
or "",
|
|
result.get("filename") or result.get("path") or "",
|
|
result.get("repo_id") or result.get("repo") or "",
|
|
result.get("model_id") or "",
|
|
result.get("version_id") or "",
|
|
result.get("name") or "",
|
|
]
|
|
return "::".join(str(part).strip() for part in parts)
|
|
|
|
def stamp_result(
|
|
self,
|
|
source_key: str,
|
|
result: Any,
|
|
*,
|
|
force_search: bool,
|
|
) -> Any:
|
|
if isinstance(result, list):
|
|
return [
|
|
self.stamp_result(
|
|
source_key,
|
|
item,
|
|
force_search=force_search,
|
|
)
|
|
for item in result
|
|
]
|
|
if not isinstance(result, dict):
|
|
return result
|
|
|
|
signature = self._result_signature(source_key, result)
|
|
if not signature:
|
|
return result
|
|
|
|
timestamp = (
|
|
result.get("searched_at")
|
|
or result.get("searchedAt")
|
|
or (
|
|
None
|
|
if force_search
|
|
else self._timestamps.get(signature)
|
|
)
|
|
)
|
|
if not timestamp:
|
|
timestamp = self._current_timestamp()
|
|
if force_search:
|
|
self._timestamps[signature] = timestamp
|
|
else:
|
|
self._timestamps.setdefault(signature, timestamp)
|
|
result["searched_at"] = timestamp
|
|
return result
|
|
|
|
def stamp_results(self, payload: dict[str, Any], *, force_search: bool):
|
|
"""Stamp all provider result collections in a search response."""
|
|
for source_key in SEARCH_RESULT_KEYS:
|
|
if payload.get(source_key):
|
|
payload[source_key] = self.stamp_result(
|
|
source_key,
|
|
payload[source_key],
|
|
force_search=force_search,
|
|
)
|
|
return payload
|