This commit is contained in:
nose
2025-12-11 19:04:02 -08:00
parent 6863c6c7ea
commit 16d8a763cd
103 changed files with 4759 additions and 9156 deletions

View File

@@ -11,7 +11,7 @@ import sys
import inspect
from collections.abc import Iterable as IterableABC
from helper.logger import log, debug
from SYS.logger import log, debug
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set
from dataclasses import dataclass, field
@@ -149,7 +149,7 @@ class SharedArgs:
@staticmethod
def get_store_choices(config: Optional[Dict[str, Any]] = None) -> List[str]:
"""Get list of available storage backend names from FileStorage.
"""Get list of available store backend names.
This method dynamically discovers all configured storage backends
instead of using a static list. Should be called when building
@@ -162,13 +162,10 @@ class SharedArgs:
List of backend names (e.g., ['default', 'test', 'home', 'work'])
Example:
# In a cmdlet that needs dynamic choices
from helper.store import FileStorage
storage = FileStorage(config)
SharedArgs.STORE.choices = SharedArgs.get_store_choices(config)
"""
try:
from helper.store import FileStorage
from Store import Store
# If no config provided, try to load it
if config is None:
@@ -178,8 +175,8 @@ class SharedArgs:
except Exception:
return []
file_storage = FileStorage(config)
return file_storage.list_backends()
store = Store(config)
return store.list_backends()
except Exception:
# Fallback to empty list if FileStorage isn't available
return []
@@ -609,7 +606,7 @@ def normalize_hash(hash_hex: Optional[str]) -> Optional[str]:
return text.lower() if text else None
def get_hash_for_operation(override_hash: Optional[str], result: Any, field_name: str = "hash_hex") -> Optional[str]:
def get_hash_for_operation(override_hash: Optional[str], result: Any, field_name: str = "hash") -> Optional[str]:
"""Get normalized hash from override or result object, consolidating common pattern.
Eliminates repeated pattern: normalize_hash(override) if override else normalize_hash(get_field(result, ...))
@@ -617,15 +614,14 @@ def get_hash_for_operation(override_hash: Optional[str], result: Any, field_name
Args:
override_hash: Hash passed as command argument (takes precedence)
result: Object containing hash field (fallback)
field_name: Name of hash field in result object (default: "hash_hex")
field_name: Name of hash field in result object (default: "hash")
Returns:
Normalized hash string, or None if neither override nor result provides valid hash
"""
if override_hash:
return normalize_hash(override_hash)
# Try multiple field names for robustness
hash_value = get_field(result, field_name) or getattr(result, field_name, None) or getattr(result, "hash", None) or result.get("file_hash") if isinstance(result, dict) else None
hash_value = get_field(result, field_name) or getattr(result, field_name, None) or getattr(result, "hash", None)
return normalize_hash(hash_value)
@@ -645,8 +641,8 @@ def fetch_hydrus_metadata(config: Any, hash_hex: str, **kwargs) -> tuple[Optiona
- metadata_dict: Dict from Hydrus (first item in metadata list) or None if unavailable
- error_code: 0 on success, 1 on any error (suitable for returning from cmdlet execute())
"""
from helper import hydrus
hydrus_wrapper = hydrus
from API import HydrusNetwork
hydrus_wrapper = HydrusNetwork
try:
client = hydrus_wrapper.get_client(config)
@@ -670,24 +666,6 @@ def fetch_hydrus_metadata(config: Any, hash_hex: str, **kwargs) -> tuple[Optiona
return meta, 0
def get_origin(obj: Any, default: Optional[str] = None) -> Optional[str]:
"""Extract origin field with fallback to store/source field, consolidating common pattern.
Supports both dict and object access patterns.
Args:
obj: Object (dict or dataclass) with 'store', 'origin', or 'source' field
default: Default value if none of the fields are found
Returns:
Store/origin/source string, or default if none exist
"""
if isinstance(obj, dict):
return obj.get("store") or obj.get("origin") or obj.get("source") or default
else:
return getattr(obj, "store", None) or getattr(obj, "origin", None) or getattr(obj, "source", None) or default
def get_field(obj: Any, field: str, default: Optional[Any] = None) -> Any:
"""Extract a field from either a dict or object with fallback default.
@@ -706,56 +684,19 @@ def get_field(obj: Any, field: str, default: Optional[Any] = None) -> Any:
Examples:
get_field(result, "hash") # From dict or object
get_field(result, "origin", "unknown") # With default
get_field(result, "table", "unknown") # With default
"""
# Handle lists by accessing the first element
if isinstance(obj, list) and obj:
obj = obj[0]
if isinstance(obj, dict):
# Direct lookup first
val = obj.get(field, default)
if val is not None:
return val
# Fallback aliases for common fields
if field == "path":
for alt in ("file_path", "target", "filepath", "file"):
v = obj.get(alt)
if v:
return v
if field == "hash":
for alt in ("file_hash", "hash_hex"):
v = obj.get(alt)
if v:
return v
if field == "store":
for alt in ("storage", "storage_source", "origin"):
v = obj.get(alt)
if v:
return v
return default
return obj.get(field, default)
else:
# Try direct attribute access first
value = getattr(obj, field, None)
if value is not None:
return value
# Attribute fallback aliases for common fields
if field == "path":
for alt in ("file_path", "target", "filepath", "file", "url"):
v = getattr(obj, alt, None)
if v:
return v
if field == "hash":
for alt in ("file_hash", "hash_hex"):
v = getattr(obj, alt, None)
if v:
return v
if field == "store":
for alt in ("storage", "storage_source", "origin"):
v = getattr(obj, alt, None)
if v:
return v
# For PipeObjects, also check the extra field
if hasattr(obj, 'extra') and isinstance(obj.extra, dict):
@@ -1148,7 +1089,7 @@ def create_pipe_object_result(
file_path: str,
cmdlet_name: str,
title: Optional[str] = None,
file_hash: Optional[str] = None,
hash_value: Optional[str] = None,
is_temp: bool = False,
parent_hash: Optional[str] = None,
tags: Optional[List[str]] = None,
@@ -1165,7 +1106,7 @@ def create_pipe_object_result(
file_path: Path to the file
cmdlet_name: Name of the cmdlet that created this (e.g., 'download-data', 'screen-shot')
title: Human-readable title
file_hash: SHA-256 hash of file (for integrity)
hash_value: SHA-256 hash of file (for integrity)
is_temp: If True, this is a temporary/intermediate artifact
parent_hash: Hash of the parent file in the chain (for provenance)
tags: List of tags to apply
@@ -1183,13 +1124,12 @@ def create_pipe_object_result(
if title:
result['title'] = title
if file_hash:
result['file_hash'] = file_hash
result['hash'] = file_hash
if hash_value:
result['hash'] = hash_value
if is_temp:
result['is_temp'] = True
if parent_hash:
result['parent_id'] = parent_hash # parent_id is the parent's file_hash
result['parent_hash'] = parent_hash
if tags:
result['tags'] = tags
@@ -1219,17 +1159,17 @@ def mark_as_temp(pipe_object: Dict[str, Any]) -> Dict[str, Any]:
return pipe_object
def set_parent_id(pipe_object: Dict[str, Any], parent_hash: str) -> Dict[str, Any]:
"""Set the parent_id for provenance tracking.
def set_parent_hash(pipe_object: Dict[str, Any], parent_hash: str) -> Dict[str, Any]:
"""Set the parent_hash for provenance tracking.
Args:
pipe_object: Result dict
parent_hash: Parent file's hash
Returns:
Modified dict with parent_id set to the hash
Modified dict with parent_hash set to the hash
"""
pipe_object['parent_id'] = parent_hash
pipe_object['parent_hash'] = parent_hash
return pipe_object
@@ -1254,13 +1194,13 @@ def get_pipe_object_hash(pipe_object: Any) -> Optional[str]:
"""Extract file hash from PipeObject, dict, or pipeline-friendly object."""
if pipe_object is None:
return None
for attr in ('file_hash', 'hash_hex', 'hash'):
for attr in ('hash',):
if hasattr(pipe_object, attr):
value = getattr(pipe_object, attr)
if value:
return value
if isinstance(pipe_object, dict):
for key in ('file_hash', 'hash_hex', 'hash'):
for key in ('hash',):
value = pipe_object.get(key)
if value:
return value
@@ -1522,13 +1462,12 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
"""
# Debug: Print ResultItem details if coming from search_file.py
try:
from helper.logger import is_debug_enabled, debug
from SYS.logger import is_debug_enabled, debug
if is_debug_enabled() and hasattr(value, '__class__') and value.__class__.__name__ == 'ResultItem':
debug("[ResultItem -> PipeObject conversion]")
debug(f" origin={getattr(value, 'origin', None)}")
debug(f" title={getattr(value, 'title', None)}")
debug(f" target={getattr(value, 'target', None)}")
debug(f" hash_hex={getattr(value, 'hash_hex', None)}")
debug(f" hash={getattr(value, 'hash', None)}")
debug(f" media_kind={getattr(value, 'media_kind', None)}")
debug(f" tags={getattr(value, 'tags', None)}")
debug(f" tag_summary={getattr(value, 'tag_summary', None)}")
@@ -1554,14 +1493,11 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
if isinstance(value, dict):
# Extract hash and store (canonical identifiers)
hash_val = value.get("hash") or value.get("file_hash")
# Recognize multiple possible store naming conventions (store, origin, storage, storage_source)
store_val = value.get("store") or value.get("origin") or value.get("storage") or value.get("storage_source") or "PATH"
# If the store value is embedded under extra, also detect it
if not store_val or store_val in ("local", "PATH"):
extra_store = None
hash_val = value.get("hash")
store_val = value.get("store") or "PATH"
if not store_val or store_val == "PATH":
try:
extra_store = value.get("extra", {}).get("store") or value.get("extra", {}).get("storage") or value.get("extra", {}).get("storage_source")
extra_store = value.get("extra", {}).get("store")
except Exception:
extra_store = None
if extra_store:
@@ -1572,7 +1508,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
path_val = value.get("path")
if path_val:
try:
from helper.utils import sha256_file
from SYS.utils import sha256_file
from pathlib import Path
hash_val = sha256_file(Path(path_val))
except Exception:
@@ -1655,7 +1591,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
relationships=rels,
is_temp=bool(value.get("is_temp", False)),
action=value.get("action"),
parent_hash=value.get("parent_hash") or value.get("parent_id"),
parent_hash=value.get("parent_hash"),
extra=extra,
)
@@ -1671,7 +1607,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
if path_val and path_val != "unknown":
try:
from helper.utils import sha256_file
from SYS.utils import sha256_file
from pathlib import Path
path_obj = Path(path_val)
hash_val = sha256_file(path_obj)
@@ -1714,7 +1650,7 @@ def register_url_with_local_library(pipe_obj: models.PipeObject, config: Dict[st
try:
from config import get_local_storage_path
from helper.folder_store import FolderDB
from API.folder import API_folder_store
file_path = get_field(pipe_obj, "path")
url_field = get_field(pipe_obj, "url", [])
@@ -1735,7 +1671,7 @@ def register_url_with_local_library(pipe_obj: models.PipeObject, config: Dict[st
if not storage_path:
return False
with FolderDB(storage_path) as db:
with API_folder_store(storage_path) as db:
file_hash = db.get_file_hash(path_obj)
if not file_hash:
return False