This commit is contained in:
nose
2025-12-11 23:21:45 -08:00
parent 16d8a763cd
commit e2ffcab030
44 changed files with 3558 additions and 1793 deletions

View File

@@ -1,7 +1,4 @@
"""Shared utilities for cmdlets and funacts.
This module provides common utility functions for working with hashes, tags,
relationship data, and other frequently-needed operations.
"""
"""
from __future__ import annotations
@@ -192,7 +189,7 @@ class SharedArgs:
DELETE_FLAG = CmdletArg(
"delete",
type="flag",
description="Delete the file and its .tags after successful operation."
description="Delete the file and its .tag after successful operation."
)
# Metadata arguments
@@ -1092,7 +1089,7 @@ def create_pipe_object_result(
hash_value: Optional[str] = None,
is_temp: bool = False,
parent_hash: Optional[str] = None,
tags: Optional[List[str]] = None,
tag: Optional[List[str]] = None,
**extra: Any
) -> Dict[str, Any]:
"""Create a PipeObject-compatible result dict for pipeline chaining.
@@ -1109,7 +1106,7 @@ def create_pipe_object_result(
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
tag: List of tag values to apply
**extra: Additional fields
Returns:
@@ -1130,8 +1127,8 @@ def create_pipe_object_result(
result['is_temp'] = True
if parent_hash:
result['parent_hash'] = parent_hash
if tags:
result['tags'] = tags
if tag:
result['tag'] = tag
# Canonical store field: use source for compatibility
try:
@@ -1350,33 +1347,46 @@ def collapse_namespace_tags(tags: Optional[Iterable[Any]], namespace: str, prefe
return result
def extract_tags_from_result(result: Any) -> list[str]:
tags: list[str] = []
if isinstance(result, models.PipeObject):
tags.extend(result.tags or [])
tags.extend(result.extra.get('tags', []))
elif hasattr(result, 'tags'):
# Handle objects with tags attribute (e.g. SearchResult)
val = getattr(result, 'tags')
if isinstance(val, (list, set, tuple)):
tags.extend(val)
elif isinstance(val, str):
tags.append(val)
if isinstance(result, dict):
raw_tags = result.get('tags')
if isinstance(raw_tags, list):
tags.extend(raw_tags)
elif isinstance(raw_tags, str):
tags.append(raw_tags)
extra = result.get('extra')
if isinstance(extra, dict):
extra_tags = extra.get('tags')
if isinstance(extra_tags, list):
tags.extend(extra_tags)
elif isinstance(extra_tags, str):
tags.append(extra_tags)
return merge_sequences(tags, case_sensitive=True)
def collapse_namespace_tag(tags: Optional[Iterable[Any]], namespace: str, prefer: str = "last") -> list[str]:
"""Singular alias for collapse_namespace_tags.
Some cmdlets prefer the singular name; keep behavior centralized.
"""
return collapse_namespace_tags(tags, namespace, prefer=prefer)
def extract_tag_from_result(result: Any) -> list[str]:
tag: list[str] = []
if isinstance(result, models.PipeObject):
tag.extend(result.tag or [])
if isinstance(result.extra, dict):
extra_tag = result.extra.get('tag')
if isinstance(extra_tag, list):
tag.extend(extra_tag)
elif isinstance(extra_tag, str):
tag.append(extra_tag)
elif hasattr(result, 'tag'):
# Handle objects with tag attribute (e.g. SearchResult)
val = getattr(result, 'tag')
if isinstance(val, (list, set, tuple)):
tag.extend(val)
elif isinstance(val, str):
tag.append(val)
if isinstance(result, dict):
raw_tag = result.get('tag')
if isinstance(raw_tag, list):
tag.extend(raw_tag)
elif isinstance(raw_tag, str):
tag.append(raw_tag)
extra = result.get('extra')
if isinstance(extra, dict):
extra_tag = extra.get('tag')
if isinstance(extra_tag, list):
tag.extend(extra_tag)
elif isinstance(extra_tag, str):
tag.append(extra_tag)
return merge_sequences(tag, case_sensitive=True)
def extract_title_from_result(result: Any) -> Optional[str]:
@@ -1469,7 +1479,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
debug(f" target={getattr(value, 'target', 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={getattr(value, 'tag', None)}")
debug(f" tag_summary={getattr(value, 'tag_summary', None)}")
debug(f" size_bytes={getattr(value, 'size_bytes', None)}")
debug(f" duration_seconds={getattr(value, 'duration_seconds', None)}")
@@ -1483,7 +1493,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
return value
known_keys = {
"hash", "store", "tags", "title", "url", "source_url", "duration", "metadata",
"hash", "store", "tag", "title", "url", "source_url", "duration", "metadata",
"warnings", "path", "relationships", "is_temp", "action", "parent_hash",
}
@@ -1542,18 +1552,14 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
# Extract relationships
rels = value.get("relationships") or {}
# Consolidate tags: prefer tags_set over tags, tag_summary
tags_val = []
if "tags_set" in value and value["tags_set"]:
tags_val = list(value["tags_set"])
elif "tags" in value and isinstance(value["tags"], (list, set)):
tags_val = list(value["tags"])
elif "tag" in value:
# Single tag string or list
if isinstance(value["tag"], list):
tags_val = value["tag"] # Already a list
else:
tags_val = [value["tag"]] # Wrap single string in list
# Canonical tag: accept list or single string
tag_val: list[str] = []
if "tag" in value:
raw_tag = value["tag"]
if isinstance(raw_tag, list):
tag_val = [str(t) for t in raw_tag if t is not None]
elif isinstance(raw_tag, str):
tag_val = [raw_tag]
# Consolidate path: prefer explicit path key, but NOT target if it's a URL
path_val = value.get("path")
@@ -1580,7 +1586,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
pipe_obj = models.PipeObject(
hash=hash_val,
store=store_val,
tags=tags_val,
tag=tag_val,
title=title_val,
url=url_val,
source_url=value.get("source_url"),
@@ -1624,7 +1630,7 @@ def coerce_to_pipe_object(value: Any, default_path: Optional[str] = None) -> mod
store=store_val,
path=str(path_val) if path_val and path_val != "unknown" else None,
title=title_val,
tags=[],
tag=[],
extra={},
)