fdf
This commit is contained in:
@@ -11,7 +11,7 @@ import sys
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||
from SYS.logger import log
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,6 +29,7 @@ _CONFIG_CACHE: Dict[str, Any] = {}
|
||||
_LAST_SAVED_CONFIG: Dict[str, Any] = {}
|
||||
_CONFIG_SAVE_MAX_RETRIES = 5
|
||||
_CONFIG_SAVE_RETRY_DELAY = 0.15
|
||||
_CONFIG_MISSING = object()
|
||||
|
||||
|
||||
class ConfigSaveConflict(Exception):
|
||||
@@ -61,6 +62,94 @@ def clear_config_cache() -> None:
|
||||
_LAST_SAVED_CONFIG = {}
|
||||
|
||||
|
||||
def get_nested_config_value(config: Dict[str, Any], *path: str) -> Any:
|
||||
cur: Any = config
|
||||
for key in path:
|
||||
if not isinstance(cur, dict):
|
||||
return None
|
||||
cur = cur.get(key)
|
||||
return cur
|
||||
|
||||
|
||||
def coerce_config_value(
|
||||
value: Any,
|
||||
existing_value: Any = _CONFIG_MISSING,
|
||||
*,
|
||||
on_error: Optional[Callable[[str], None]] = None,
|
||||
) -> Any:
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
text = value.strip()
|
||||
lowered = text.lower()
|
||||
|
||||
if existing_value is _CONFIG_MISSING:
|
||||
if lowered in {"true", "false"}:
|
||||
return lowered == "true"
|
||||
if text.isdigit():
|
||||
return int(text)
|
||||
return value
|
||||
|
||||
if isinstance(existing_value, bool):
|
||||
if lowered in {"true", "yes", "1", "on"}:
|
||||
return True
|
||||
if lowered in {"false", "no", "0", "off"}:
|
||||
return False
|
||||
if on_error is not None:
|
||||
on_error(f"Warning: Could not convert '{value}' to boolean. Using string.")
|
||||
return value
|
||||
|
||||
if isinstance(existing_value, int) and not isinstance(existing_value, bool):
|
||||
try:
|
||||
return int(text)
|
||||
except ValueError:
|
||||
if on_error is not None:
|
||||
on_error(f"Warning: Could not convert '{value}' to int. Using string.")
|
||||
return value
|
||||
|
||||
if isinstance(existing_value, float):
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
if on_error is not None:
|
||||
on_error(f"Warning: Could not convert '{value}' to float. Using string.")
|
||||
return value
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def set_nested_config_value(
|
||||
config: Dict[str, Any],
|
||||
key_path: str | Sequence[str],
|
||||
value: Any,
|
||||
*,
|
||||
on_error: Optional[Callable[[str], None]] = None,
|
||||
) -> bool:
|
||||
if not isinstance(config, dict):
|
||||
return False
|
||||
|
||||
if isinstance(key_path, str):
|
||||
keys = [part for part in key_path.split(".") if part]
|
||||
else:
|
||||
keys = [str(part) for part in (key_path or []) if str(part)]
|
||||
|
||||
if not keys:
|
||||
return False
|
||||
|
||||
current = config
|
||||
for key in keys[:-1]:
|
||||
next_value = current.get(key)
|
||||
if not isinstance(next_value, dict):
|
||||
next_value = {}
|
||||
current[key] = next_value
|
||||
current = next_value
|
||||
|
||||
last_key = keys[-1]
|
||||
existing_value = current[last_key] if last_key in current else _CONFIG_MISSING
|
||||
current[last_key] = coerce_config_value(value, existing_value, on_error=on_error)
|
||||
return True
|
||||
|
||||
|
||||
def get_hydrus_instance(
|
||||
config: Dict[str, Any], instance_name: str = "home"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
104
SYS/detail_view_helpers.py
Normal file
104
SYS/detail_view_helpers.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, Optional, Sequence
|
||||
|
||||
|
||||
def _labelize_key(key: str) -> str:
|
||||
return str(key or "").replace("_", " ").title()
|
||||
|
||||
|
||||
def _normalize_tags_value(tags: Any) -> Optional[str]:
|
||||
if tags is None:
|
||||
return None
|
||||
if isinstance(tags, str):
|
||||
text = tags.strip()
|
||||
return text or None
|
||||
if isinstance(tags, Sequence):
|
||||
seen: list[str] = []
|
||||
for tag in tags:
|
||||
text = str(tag or "").strip()
|
||||
if text and text not in seen:
|
||||
seen.append(text)
|
||||
return ", ".join(seen) if seen else None
|
||||
text = str(tags).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def prepare_detail_metadata(
|
||||
subject: Any,
|
||||
*,
|
||||
include_subject_fields: bool = False,
|
||||
title: Optional[str] = None,
|
||||
hash_value: Optional[str] = None,
|
||||
store: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
tags: Any = None,
|
||||
prefer_existing_tags: bool = True,
|
||||
extra_fields: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
from SYS.result_table import extract_item_metadata
|
||||
|
||||
metadata = extract_item_metadata(subject) or {}
|
||||
|
||||
if include_subject_fields and isinstance(subject, dict):
|
||||
for key, value in subject.items():
|
||||
if str(key).startswith("_") or key in {"selection_action", "selection_args"}:
|
||||
continue
|
||||
label = _labelize_key(str(key))
|
||||
if label not in metadata and value is not None:
|
||||
metadata[label] = value
|
||||
|
||||
if title:
|
||||
metadata["Title"] = title
|
||||
if hash_value:
|
||||
metadata["Hash"] = hash_value
|
||||
if store:
|
||||
metadata["Store"] = store
|
||||
if path:
|
||||
metadata["Path"] = path
|
||||
|
||||
tags_text = _normalize_tags_value(tags)
|
||||
if tags_text and (not prefer_existing_tags or not metadata.get("Tags")):
|
||||
metadata["Tags"] = tags_text
|
||||
|
||||
for key, value in (extra_fields or {}).items():
|
||||
if value is not None:
|
||||
metadata[str(key)] = value
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def create_detail_view(
|
||||
title: str,
|
||||
metadata: dict[str, Any],
|
||||
*,
|
||||
table_name: Optional[str] = None,
|
||||
source_command: Optional[tuple[str, Sequence[str]]] = None,
|
||||
init_command: Optional[tuple[str, Sequence[str]]] = None,
|
||||
max_columns: Optional[int] = None,
|
||||
exclude_tags: bool = False,
|
||||
value_case: Optional[str] = "preserve",
|
||||
perseverance: bool = True,
|
||||
) -> Any:
|
||||
from SYS.result_table import ItemDetailView
|
||||
|
||||
kwargs: dict[str, Any] = {"item_metadata": metadata}
|
||||
if max_columns is not None:
|
||||
kwargs["max_columns"] = max_columns
|
||||
if exclude_tags:
|
||||
kwargs["exclude_tags"] = True
|
||||
|
||||
table = ItemDetailView(title, **kwargs)
|
||||
if table_name:
|
||||
table = table.set_table(table_name)
|
||||
if value_case:
|
||||
table = table.set_value_case(value_case)
|
||||
if perseverance:
|
||||
table = table._perseverance(True)
|
||||
if source_command:
|
||||
name, args = source_command
|
||||
table.set_source_command(name, list(args))
|
||||
if init_command:
|
||||
name, args = init_command
|
||||
table = table.init_command(name, list(args))
|
||||
return table
|
||||
132
SYS/item_accessors.py
Normal file
132
SYS/item_accessors.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
||||
|
||||
|
||||
def get_field(obj: Any, field: str, default: Optional[Any] = None) -> Any:
|
||||
if isinstance(obj, list):
|
||||
if not obj:
|
||||
return default
|
||||
obj = obj[0]
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(field, default)
|
||||
|
||||
value = getattr(obj, field, None)
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
extra_value = getattr(obj, "extra", None)
|
||||
if isinstance(extra_value, dict):
|
||||
return extra_value.get(field, default)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def first_field(obj: Any, fields: Iterable[str], default: Optional[Any] = None) -> Any:
|
||||
for field in fields:
|
||||
value = get_field(obj, str(field), None)
|
||||
if value is not None:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def get_text_field(obj: Any, *fields: str, default: str = "") -> str:
|
||||
value = first_field(obj, fields, default=None)
|
||||
if value is None:
|
||||
return default
|
||||
text = str(value).strip()
|
||||
return text if text else default
|
||||
|
||||
|
||||
def get_column_text(obj: Any, *labels: str) -> Optional[str]:
|
||||
columns = get_field(obj, "columns")
|
||||
if not isinstance(columns, list):
|
||||
return None
|
||||
wanted = {str(label or "").strip().lower() for label in labels if str(label or "").strip()}
|
||||
if not wanted:
|
||||
return None
|
||||
for pair in columns:
|
||||
try:
|
||||
if not isinstance(pair, (list, tuple)) or len(pair) != 2:
|
||||
continue
|
||||
key, value = pair
|
||||
if str(key or "").strip().lower() not in wanted:
|
||||
continue
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
return text
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def get_int_field(obj: Any, *fields: str) -> Optional[int]:
|
||||
value = first_field(obj, fields, default=None)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_extension_field(obj: Any, *fields: str) -> str:
|
||||
text = get_text_field(obj, *(fields or ("ext", "extension")), default="")
|
||||
return text.lstrip(".") if text else ""
|
||||
|
||||
|
||||
def get_result_title(obj: Any, *fields: str) -> Optional[str]:
|
||||
text = get_text_field(obj, *(fields or ("title", "name", "filename")), default="")
|
||||
if text:
|
||||
return text
|
||||
return get_column_text(obj, "title", "name")
|
||||
|
||||
|
||||
def extract_item_tags(obj: Any) -> list[str]:
|
||||
return get_string_list(obj, "tag")
|
||||
|
||||
|
||||
def get_string_list(obj: Any, field: str) -> list[str]:
|
||||
value = get_field(obj, field)
|
||||
if isinstance(value, list):
|
||||
return [str(item).strip() for item in value if item is not None and str(item).strip()]
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
return [text] if text else []
|
||||
return []
|
||||
|
||||
|
||||
def set_field(obj: Any, field: str, value: Any) -> bool:
|
||||
if isinstance(obj, dict):
|
||||
obj[field] = value
|
||||
return True
|
||||
try:
|
||||
setattr(obj, field, value)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_sha256_hex(obj: Any, *fields: str) -> Optional[str]:
|
||||
value = get_text_field(obj, *(fields or ("hash",)))
|
||||
if value and _SHA256_RE.fullmatch(value):
|
||||
return value.lower()
|
||||
return None
|
||||
|
||||
|
||||
def get_store_name(obj: Any, *fields: str) -> Optional[str]:
|
||||
value = get_text_field(obj, *(fields or ("store",)))
|
||||
return value or None
|
||||
|
||||
|
||||
def get_http_url(obj: Any, *fields: str) -> Optional[str]:
|
||||
value = get_text_field(obj, *(fields or ("url", "target")))
|
||||
if value.lower().startswith(("http://", "https://")):
|
||||
return value
|
||||
return None
|
||||
158
SYS/payload_builders.py
Normal file
158
SYS/payload_builders.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
|
||||
def normalize_file_extension(ext_value: Any) -> str:
|
||||
ext = str(ext_value or "").strip().lstrip(".")
|
||||
for sep in (" ", "|", "(", "[", "{", ",", ";"):
|
||||
if sep in ext:
|
||||
ext = ext.split(sep, 1)[0]
|
||||
break
|
||||
if "." in ext:
|
||||
ext = ext.split(".")[-1]
|
||||
ext = "".join(ch for ch in ext if ch.isalnum())
|
||||
return ext[:5]
|
||||
|
||||
|
||||
def extract_title_tag_value(tags: Sequence[str]) -> Optional[str]:
|
||||
for tag in tags:
|
||||
text = str(tag or "").strip()
|
||||
if text.lower().startswith("title:"):
|
||||
value = text.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _derive_title(
|
||||
title: Optional[str],
|
||||
fallback_title: Optional[str],
|
||||
path: Optional[str],
|
||||
url: Any,
|
||||
hash_value: Optional[str],
|
||||
) -> str:
|
||||
for candidate in (title, fallback_title):
|
||||
text = str(candidate or "").strip()
|
||||
if text:
|
||||
return text
|
||||
|
||||
path_text = str(path or "").strip()
|
||||
if path_text:
|
||||
try:
|
||||
return Path(path_text).stem or path_text
|
||||
except Exception:
|
||||
return path_text
|
||||
|
||||
if isinstance(url, str):
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
name = Path(unquote(parsed.path)).stem
|
||||
if name:
|
||||
return name
|
||||
except Exception:
|
||||
pass
|
||||
text = url.strip()
|
||||
if text:
|
||||
return text
|
||||
|
||||
if isinstance(url, list):
|
||||
for candidate in url:
|
||||
text = str(candidate or "").strip()
|
||||
if text:
|
||||
return text
|
||||
|
||||
return str(hash_value or "").strip()
|
||||
|
||||
|
||||
def build_file_result_payload(
|
||||
*,
|
||||
title: Optional[str] = None,
|
||||
fallback_title: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
url: Any = None,
|
||||
hash_value: Optional[str] = None,
|
||||
store: Optional[str] = None,
|
||||
tag: Optional[Sequence[str]] = None,
|
||||
ext: Any = None,
|
||||
size_bytes: Optional[int] = None,
|
||||
columns: Optional[Sequence[tuple[str, Any]]] = None,
|
||||
source: Optional[str] = None,
|
||||
table: Optional[str] = None,
|
||||
detail: Optional[str] = None,
|
||||
**extra: Any,
|
||||
) -> Dict[str, Any]:
|
||||
resolved_title = _derive_title(title, fallback_title, path, url, hash_value)
|
||||
resolved_path = str(path).strip() if path is not None and str(path).strip() else None
|
||||
resolved_store = str(store).strip() if store is not None and str(store).strip() else None
|
||||
resolved_ext = normalize_file_extension(ext)
|
||||
if not resolved_ext:
|
||||
for candidate in (resolved_path, resolved_title):
|
||||
text = str(candidate or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
try:
|
||||
resolved_ext = normalize_file_extension(Path(text).suffix)
|
||||
except Exception:
|
||||
resolved_ext = ""
|
||||
if resolved_ext:
|
||||
break
|
||||
|
||||
payload: Dict[str, Any] = {"title": resolved_title}
|
||||
|
||||
if resolved_path is not None:
|
||||
payload["path"] = resolved_path
|
||||
if hash_value:
|
||||
payload["hash"] = str(hash_value)
|
||||
if url not in (None, "", []):
|
||||
payload["url"] = url
|
||||
if resolved_store is not None:
|
||||
payload["store"] = resolved_store
|
||||
if tag is not None:
|
||||
payload["tag"] = list(tag)
|
||||
if resolved_ext:
|
||||
payload["ext"] = resolved_ext
|
||||
if size_bytes is not None:
|
||||
payload["size_bytes"] = size_bytes
|
||||
if columns is not None:
|
||||
payload["columns"] = list(columns)
|
||||
if source:
|
||||
payload["source"] = source
|
||||
if table:
|
||||
payload["table"] = table
|
||||
if detail is not None:
|
||||
payload["detail"] = str(detail)
|
||||
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def build_table_result_payload(
|
||||
*,
|
||||
columns: Sequence[tuple[str, Any]],
|
||||
title: Optional[str] = None,
|
||||
table: Optional[str] = None,
|
||||
detail: Optional[str] = None,
|
||||
selection_args: Optional[Sequence[Any]] = None,
|
||||
selection_action: Optional[Sequence[Any]] = None,
|
||||
**extra: Any,
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"columns": [(str(label), value) for label, value in columns],
|
||||
}
|
||||
|
||||
if title is not None:
|
||||
payload["title"] = str(title)
|
||||
if table:
|
||||
payload["table"] = table
|
||||
if detail is not None:
|
||||
payload["detail"] = str(detail)
|
||||
if selection_args:
|
||||
payload["_selection_args"] = [str(arg) for arg in selection_args if arg is not None]
|
||||
if selection_action:
|
||||
payload["_selection_action"] = [str(arg) for arg in selection_action if arg is not None]
|
||||
|
||||
payload.update(extra)
|
||||
return payload
|
||||
60
SYS/result_publication.py
Normal file
60
SYS/result_publication.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
|
||||
def resolve_publication_subject(
|
||||
items: Iterable[Any] | None,
|
||||
subject: Any = None,
|
||||
) -> Any:
|
||||
if subject is not None:
|
||||
return subject
|
||||
resolved_items = list(items or [])
|
||||
if not resolved_items:
|
||||
return None
|
||||
if len(resolved_items) == 1:
|
||||
return resolved_items[0]
|
||||
return resolved_items
|
||||
|
||||
|
||||
def publish_result_table(
|
||||
pipeline_context: Any,
|
||||
result_table: Any,
|
||||
items: Iterable[Any] | None = None,
|
||||
*,
|
||||
subject: Any = None,
|
||||
overlay: bool = False,
|
||||
) -> None:
|
||||
resolved_items = list(items or [])
|
||||
resolved_subject = resolve_publication_subject(resolved_items, subject)
|
||||
if overlay:
|
||||
pipeline_context.set_last_result_table_overlay(
|
||||
result_table,
|
||||
resolved_items,
|
||||
subject=resolved_subject,
|
||||
)
|
||||
return
|
||||
pipeline_context.set_last_result_table(
|
||||
result_table,
|
||||
resolved_items,
|
||||
subject=resolved_subject,
|
||||
)
|
||||
|
||||
|
||||
def overlay_existing_result_table(
|
||||
pipeline_context: Any,
|
||||
*,
|
||||
subject: Any = None,
|
||||
) -> bool:
|
||||
table = pipeline_context.get_last_result_table()
|
||||
items = list(pipeline_context.get_last_result_items() or [])
|
||||
if table is None or not items:
|
||||
return False
|
||||
publish_result_table(
|
||||
pipeline_context,
|
||||
table,
|
||||
items,
|
||||
subject=subject,
|
||||
overlay=True,
|
||||
)
|
||||
return True
|
||||
10
SYS/result_table_helpers.py
Normal file
10
SYS/result_table_helpers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
def add_row_columns(table: Any, columns: Iterable[tuple[str, Any]]) -> Any:
|
||||
row = table.add_row()
|
||||
for label, value in columns:
|
||||
row.add_column(str(label), "" if value is None else str(value))
|
||||
return row
|
||||
157
SYS/selection_builder.py
Normal file
157
SYS/selection_builder.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
|
||||
_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
||||
|
||||
|
||||
def looks_like_url(value: Any, *, extra_prefixes: Iterable[str] = ()) -> bool:
|
||||
try:
|
||||
text = str(value or "").strip().lower()
|
||||
except Exception:
|
||||
return False
|
||||
if not text:
|
||||
return False
|
||||
prefixes = ("http://", "https://", "magnet:", "torrent:") + tuple(
|
||||
str(prefix).strip().lower() for prefix in extra_prefixes if str(prefix).strip()
|
||||
)
|
||||
return text.startswith(prefixes)
|
||||
|
||||
|
||||
def normalize_selection_args(selection_args: Any) -> Optional[List[str]]:
|
||||
if isinstance(selection_args, (list, tuple)):
|
||||
return [str(arg) for arg in selection_args if arg is not None]
|
||||
if selection_args is not None:
|
||||
return [str(selection_args)]
|
||||
return None
|
||||
|
||||
|
||||
def normalize_hash_for_selection(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if _SHA256_RE.fullmatch(text):
|
||||
return text.lower()
|
||||
return text
|
||||
|
||||
|
||||
def build_hash_store_selection(
|
||||
hash_value: Any,
|
||||
store_value: Any,
|
||||
*,
|
||||
action_name: str = "get-metadata",
|
||||
) -> Tuple[Optional[List[str]], Optional[List[str]]]:
|
||||
hash_text = normalize_hash_for_selection(hash_value)
|
||||
store_text = str(store_value or "").strip()
|
||||
if not hash_text or not store_text:
|
||||
return None, None
|
||||
args = ["-query", f"hash:{hash_text}", "-store", store_text]
|
||||
return args, [action_name] + list(args)
|
||||
|
||||
|
||||
def build_default_selection(
|
||||
*,
|
||||
path_value: Any,
|
||||
hash_value: Any = None,
|
||||
store_value: Any = None,
|
||||
) -> Tuple[Optional[List[str]], Optional[List[str]]]:
|
||||
path_text = str(path_value or "").strip()
|
||||
hash_args, hash_action = build_hash_store_selection(hash_value, store_value)
|
||||
|
||||
if path_text:
|
||||
if looks_like_url(path_text):
|
||||
if hash_args and "/view_file" in path_text:
|
||||
return hash_args, hash_action
|
||||
args = ["-url", path_text]
|
||||
return args, ["download-file", "-url", path_text]
|
||||
|
||||
if hash_args:
|
||||
return hash_args, hash_action
|
||||
|
||||
try:
|
||||
from SYS.utils import expand_path
|
||||
|
||||
resolved_path = str(expand_path(path_text))
|
||||
except Exception:
|
||||
resolved_path = path_text
|
||||
|
||||
args = ["-path", resolved_path]
|
||||
return args, ["get-file", "-path", resolved_path]
|
||||
|
||||
return hash_args, hash_action
|
||||
|
||||
|
||||
def extract_selection_fields(
|
||||
item: Any,
|
||||
*,
|
||||
extra_url_prefixes: Iterable[str] = (),
|
||||
) -> Tuple[Optional[List[str]], Optional[List[str]], Optional[str]]:
|
||||
selection_args: Any = None
|
||||
selection_action: Any = None
|
||||
item_url: Any = None
|
||||
|
||||
if isinstance(item, dict):
|
||||
selection_args = item.get("_selection_args") or item.get("selection_args")
|
||||
selection_action = item.get("_selection_action") or item.get("selection_action")
|
||||
item_url = item.get("url") or item.get("path") or item.get("target")
|
||||
nested_values = [item.get("metadata"), item.get("full_metadata"), item.get("extra")]
|
||||
else:
|
||||
item_url = getattr(item, "url", None) or getattr(item, "path", None) or getattr(item, "target", None)
|
||||
nested_values = [
|
||||
getattr(item, "metadata", None),
|
||||
getattr(item, "full_metadata", None),
|
||||
getattr(item, "extra", None),
|
||||
]
|
||||
|
||||
for nested in nested_values:
|
||||
if not isinstance(nested, dict):
|
||||
continue
|
||||
selection_args = selection_args or nested.get("_selection_args") or nested.get("selection_args")
|
||||
selection_action = selection_action or nested.get("_selection_action") or nested.get("selection_action")
|
||||
item_url = item_url or nested.get("url") or nested.get("source_url") or nested.get("target")
|
||||
|
||||
normalized_args = normalize_selection_args(selection_args)
|
||||
normalized_action = normalize_selection_args(selection_action)
|
||||
|
||||
if item_url and not looks_like_url(item_url, extra_prefixes=extra_url_prefixes):
|
||||
item_url = None
|
||||
|
||||
return normalized_args, normalized_action, str(item_url) if item_url else None
|
||||
|
||||
|
||||
def selection_args_have_url(
|
||||
args_list: Sequence[str],
|
||||
*,
|
||||
extra_url_prefixes: Iterable[str] = (),
|
||||
) -> bool:
|
||||
for idx, arg in enumerate(args_list):
|
||||
low = str(arg or "").strip().lower()
|
||||
if low in {"-url", "--url"} and idx + 1 < len(args_list):
|
||||
return True
|
||||
if looks_like_url(arg, extra_prefixes=extra_url_prefixes):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_urls_from_selection_args(
|
||||
args_list: Sequence[str],
|
||||
*,
|
||||
extra_url_prefixes: Iterable[str] = (),
|
||||
) -> List[str]:
|
||||
urls: List[str] = []
|
||||
idx = 0
|
||||
while idx < len(args_list):
|
||||
token = str(args_list[idx] or "")
|
||||
low = token.strip().lower()
|
||||
if low in {"-url", "--url"} and idx + 1 < len(args_list):
|
||||
candidate = str(args_list[idx + 1] or "").strip()
|
||||
if looks_like_url(candidate, extra_prefixes=extra_url_prefixes) and candidate not in urls:
|
||||
urls.append(candidate)
|
||||
idx += 2
|
||||
continue
|
||||
if looks_like_url(token, extra_prefixes=extra_url_prefixes):
|
||||
candidate = token.strip()
|
||||
if candidate not in urls:
|
||||
urls.append(candidate)
|
||||
idx += 1
|
||||
return urls
|
||||
Reference in New Issue
Block a user