This commit is contained in:
nose
2025-12-16 23:23:43 -08:00
parent 9873280f0e
commit 86918f2ae2
46 changed files with 2277 additions and 1347 deletions

View File

@@ -244,6 +244,8 @@ class HTTPClient:
self,
method: str,
url: str,
raise_for_status: bool = True,
log_http_errors: bool = True,
**kwargs
) -> httpx.Response:
"""
@@ -273,7 +275,8 @@ class HTTPClient:
for attempt in range(self.retries):
try:
response = self._client.request(method, url, **kwargs)
response.raise_for_status()
if raise_for_status:
response.raise_for_status()
return response
except httpx.TimeoutException as e:
last_exception = e
@@ -287,7 +290,8 @@ class HTTPClient:
response_text = e.response.text[:500]
except:
response_text = "<unable to read response>"
logger.error(f"HTTP {e.response.status_code} from {url}: {response_text}")
if log_http_errors:
logger.error(f"HTTP {e.response.status_code} from {url}: {response_text}")
raise
last_exception = e
try:

View File

@@ -71,6 +71,7 @@ class HydrusNetwork:
url: str
access_key: str = ""
timeout: float = 60.0
instance_name: str = "" # Optional store name (e.g., 'home') for namespaced logs
scheme: str = field(init=False)
hostname: str = field(init=False)
@@ -90,6 +91,12 @@ class HydrusNetwork:
self.port = parsed.port or (443 if self.scheme == "https" else 80)
self.base_path = parsed.path.rstrip("/")
self.access_key = self.access_key or ""
self.instance_name = str(self.instance_name or "").strip()
def _log_prefix(self) -> str:
if self.instance_name:
return f"[hydrusnetwork:{self.instance_name}]"
return f"[hydrusnetwork:{self.hostname}:{self.port}]"
# ------------------------------------------------------------------
# low-level helpers
@@ -120,7 +127,7 @@ class HydrusNetwork:
url = f"{self.scheme}://{self.hostname}:{self.port}{path}"
# Log request details
logger.debug(f"[Hydrus] {spec.method} {spec.endpoint} (auth: {'session_key' if self._session_key else 'access_key' if self.access_key else 'none'})")
logger.debug(f"{self._log_prefix()} {spec.method} {spec.endpoint} (auth: {'session_key' if self._session_key else 'access_key' if self.access_key else 'none'})")
status = 0
reason = ""
@@ -135,14 +142,14 @@ class HydrusNetwork:
file_path = Path(spec.file_path)
if not file_path.is_file():
error_msg = f"Upload file not found: {file_path}"
logger.error(f"[Hydrus] {error_msg}")
logger.error(f"{self._log_prefix()} {error_msg}")
raise FileNotFoundError(error_msg)
file_size = file_path.stat().st_size
headers["Content-Type"] = spec.content_type or "application/octet-stream"
headers["Content-Length"] = str(file_size)
logger.debug(f"[Hydrus] Uploading file {file_path.name} ({file_size} bytes)")
logger.debug(f"{self._log_prefix()} Uploading file {file_path.name} ({file_size} bytes)")
def file_gen():
with file_path.open("rb") as handle:
@@ -153,7 +160,9 @@ class HydrusNetwork:
spec.method,
url,
content=file_gen(),
headers=headers
headers=headers,
raise_for_status=False,
log_http_errors=False,
)
else:
content = None
@@ -163,14 +172,16 @@ class HydrusNetwork:
content = spec.data
else:
json_data = spec.data
logger.debug(f"[Hydrus] Request body size: {len(content) if content else 'json'}")
logger.debug(f"{self._log_prefix()} Request body size: {len(content) if content else 'json'}")
response = client.request(
spec.method,
url,
content=content,
json=json_data,
headers=headers
headers=headers,
raise_for_status=False,
log_http_errors=False,
)
status = response.status_code
@@ -178,20 +189,14 @@ class HydrusNetwork:
body = response.content
content_type = response.headers.get("Content-Type", "") or ""
logger.debug(f"[Hydrus] Response {status} {reason} ({len(body)} bytes)")
logger.debug(f"{self._log_prefix()} Response {status} {reason} ({len(body)} bytes)")
except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as exc:
msg = f"Hydrus unavailable: {exc}"
logger.warning(f"[Hydrus] {msg}")
logger.warning(f"{self._log_prefix()} {msg}")
raise HydrusConnectionError(msg) from exc
except httpx.HTTPStatusError as exc:
response = exc.response
status = response.status_code
reason = response.reason_phrase
body = response.content
content_type = response.headers.get("Content-Type", "") or ""
except Exception as exc:
logger.error(f"[Hydrus] Connection error: {exc}", exc_info=True)
logger.error(f"{self._log_prefix()} Connection error: {exc}", exc_info=True)
raise
payload: Any
@@ -219,19 +224,23 @@ class HydrusNetwork:
message = payload
else:
message = reason or "HTTP error"
logger.error(f"[Hydrus] HTTP {status}: {message}")
# Some endpoints are naturally "missing" sometimes and should not spam logs.
if status == 404 and spec.endpoint.rstrip("/") == "/get_files/file_path":
return {}
logger.error(f"{self._log_prefix()} HTTP {status}: {message}")
# Handle expired session key (419) by clearing cache and retrying once
if status == 419 and self._session_key and "session" in message.lower():
logger.warning(f"[Hydrus] Session key expired, acquiring new one and retrying...")
logger.warning(f"{self._log_prefix()} Session key expired, acquiring new one and retrying...")
self._session_key = "" # Clear expired session key
try:
self._acquire_session_key()
# Retry the request with new session key
return self._perform_request(spec)
except Exception as retry_error:
logger.error(f"[Hydrus] Retry failed: {retry_error}", exc_info=True)
logger.error(f"{self._log_prefix()} Retry failed: {retry_error}", exc_info=True)
# If retry fails, raise the original error
raise HydrusRequestError(status, message, payload) from retry_error
@@ -316,6 +325,16 @@ class HydrusNetwork:
def add_file(self, file_path: Path) -> dict[str, Any]:
return self._post("/add_files/add_file", file_path=file_path)
def undelete_files(self, hashes: Union[str, Iterable[str]]) -> dict[str, Any]:
"""Restore files from Hydrus trash back into 'my files'.
Hydrus Client API: POST /add_files/undelete_files
Required JSON args: {"hashes": [<sha256 hex>, ...]}
"""
hash_list = self._ensure_hashes(hashes)
body = {"hashes": hash_list}
return self._post("/add_files/undelete_files", data=body)
def add_tag(self, hash: Union[str, Iterable[str]], tags: Iterable[str], service_name: str) -> dict[str, Any]:
hash = self._ensure_hashes(hash)
body = {"hashes": hash, "service_names_to_tags": {service_name: list(tags)}}