Squashed 'core/' content from commit 4957443
git-subtree-dir: core git-subtree-split: 4957443184ae0eb6323635a90a19acffb3e01d07
This commit is contained in:
259
artdag/effects/runner.py
Normal file
259
artdag/effects/runner.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Effect runner.
|
||||
|
||||
Main entry point for executing cached effects with sandboxing.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .binding import AnalysisData, bindings_to_lookup_table, resolve_all_bindings
|
||||
from .loader import load_effect, LoadedEffect
|
||||
from .meta import ExecutionContext
|
||||
from .sandbox import Sandbox, SandboxConfig, SandboxResult, get_venv_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_effect(
|
||||
effect_source: str,
|
||||
input_paths: List[Path],
|
||||
output_path: Path,
|
||||
params: Dict[str, Any],
|
||||
analysis: Optional[AnalysisData] = None,
|
||||
cache_id: str = None,
|
||||
seed: int = 0,
|
||||
trust_level: str = "untrusted",
|
||||
timeout: int = 3600,
|
||||
) -> SandboxResult:
|
||||
"""
|
||||
Run an effect with full sandboxing.
|
||||
|
||||
This is the main entry point for effect execution.
|
||||
|
||||
Args:
|
||||
effect_source: Effect source code
|
||||
input_paths: List of input file paths
|
||||
output_path: Output file path
|
||||
params: Effect parameters (may contain bindings)
|
||||
analysis: Optional analysis data for binding resolution
|
||||
cache_id: Cache ID for deterministic seeding
|
||||
seed: RNG seed (overrides cache_id-based seed)
|
||||
trust_level: "untrusted" or "trusted"
|
||||
timeout: Maximum execution time in seconds
|
||||
|
||||
Returns:
|
||||
SandboxResult with success status and output
|
||||
"""
|
||||
# Load and validate effect
|
||||
loaded = load_effect(effect_source)
|
||||
logger.info(f"Running effect '{loaded.meta.name}' v{loaded.meta.version}")
|
||||
|
||||
# Resolve bindings if analysis data available
|
||||
bindings = {}
|
||||
if analysis:
|
||||
resolved = resolve_all_bindings(params, analysis, cache_id)
|
||||
bindings = bindings_to_lookup_table(resolved)
|
||||
# Remove binding dicts from params, keeping only resolved values
|
||||
params = {
|
||||
k: v for k, v in params.items()
|
||||
if not (isinstance(v, dict) and v.get("_binding"))
|
||||
}
|
||||
|
||||
# Validate parameters
|
||||
validated_params = loaded.meta.validate_params(params)
|
||||
|
||||
# Get or create venv for dependencies
|
||||
venv_path = None
|
||||
if loaded.dependencies:
|
||||
venv_path = get_venv_path(loaded.dependencies)
|
||||
|
||||
# Configure sandbox
|
||||
config = SandboxConfig(
|
||||
trust_level=trust_level,
|
||||
venv_path=venv_path,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# Write effect to temp file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
suffix=".py",
|
||||
delete=False,
|
||||
) as f:
|
||||
f.write(effect_source)
|
||||
effect_path = Path(f.name)
|
||||
|
||||
try:
|
||||
with Sandbox(config) as sandbox:
|
||||
result = sandbox.run_effect(
|
||||
effect_path=effect_path,
|
||||
input_paths=input_paths,
|
||||
output_path=output_path,
|
||||
params=validated_params,
|
||||
bindings=bindings,
|
||||
seed=seed,
|
||||
)
|
||||
finally:
|
||||
effect_path.unlink(missing_ok=True)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def run_effect_from_cache(
|
||||
cache,
|
||||
effect_hash: str,
|
||||
input_paths: List[Path],
|
||||
output_path: Path,
|
||||
params: Dict[str, Any],
|
||||
analysis: Optional[AnalysisData] = None,
|
||||
cache_id: str = None,
|
||||
seed: int = 0,
|
||||
trust_level: str = "untrusted",
|
||||
timeout: int = 3600,
|
||||
) -> SandboxResult:
|
||||
"""
|
||||
Run an effect from cache by content hash.
|
||||
|
||||
Args:
|
||||
cache: Cache instance
|
||||
effect_hash: Content hash of effect
|
||||
input_paths: Input file paths
|
||||
output_path: Output file path
|
||||
params: Effect parameters
|
||||
analysis: Optional analysis data
|
||||
cache_id: Cache ID for seeding
|
||||
seed: RNG seed
|
||||
trust_level: "untrusted" or "trusted"
|
||||
timeout: Max execution time
|
||||
|
||||
Returns:
|
||||
SandboxResult
|
||||
"""
|
||||
effect_source = cache.get_effect(effect_hash)
|
||||
if not effect_source:
|
||||
return SandboxResult(
|
||||
success=False,
|
||||
error=f"Effect not found in cache: {effect_hash[:16]}...",
|
||||
)
|
||||
|
||||
return run_effect(
|
||||
effect_source=effect_source,
|
||||
input_paths=input_paths,
|
||||
output_path=output_path,
|
||||
params=params,
|
||||
analysis=analysis,
|
||||
cache_id=cache_id,
|
||||
seed=seed,
|
||||
trust_level=trust_level,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def check_effect_temporal(cache, effect_hash: str) -> bool:
|
||||
"""
|
||||
Check if an effect is temporal (can't be collapsed).
|
||||
|
||||
Args:
|
||||
cache: Cache instance
|
||||
effect_hash: Content hash of effect
|
||||
|
||||
Returns:
|
||||
True if effect is temporal
|
||||
"""
|
||||
metadata = cache.get_effect_metadata(effect_hash)
|
||||
if not metadata:
|
||||
return False
|
||||
|
||||
meta = metadata.get("meta", {})
|
||||
return meta.get("temporal", False)
|
||||
|
||||
|
||||
def get_effect_api_type(cache, effect_hash: str) -> str:
|
||||
"""
|
||||
Get the API type of an effect.
|
||||
|
||||
Args:
|
||||
cache: Cache instance
|
||||
effect_hash: Content hash of effect
|
||||
|
||||
Returns:
|
||||
"frame" or "video"
|
||||
"""
|
||||
metadata = cache.get_effect_metadata(effect_hash)
|
||||
if not metadata:
|
||||
return "frame"
|
||||
|
||||
meta = metadata.get("meta", {})
|
||||
return meta.get("api_type", "frame")
|
||||
|
||||
|
||||
class EffectExecutor:
|
||||
"""
|
||||
Executor for cached effects.
|
||||
|
||||
Provides a higher-level interface for effect execution.
|
||||
"""
|
||||
|
||||
def __init__(self, cache, trust_level: str = "untrusted"):
|
||||
"""
|
||||
Initialize executor.
|
||||
|
||||
Args:
|
||||
cache: Cache instance
|
||||
trust_level: Default trust level
|
||||
"""
|
||||
self.cache = cache
|
||||
self.trust_level = trust_level
|
||||
|
||||
def execute(
|
||||
self,
|
||||
effect_hash: str,
|
||||
input_paths: List[Path],
|
||||
output_path: Path,
|
||||
params: Dict[str, Any],
|
||||
analysis: Optional[AnalysisData] = None,
|
||||
step_cache_id: str = None,
|
||||
) -> SandboxResult:
|
||||
"""
|
||||
Execute an effect.
|
||||
|
||||
Args:
|
||||
effect_hash: Content hash of effect
|
||||
input_paths: Input file paths
|
||||
output_path: Output path
|
||||
params: Effect parameters
|
||||
analysis: Analysis data for bindings
|
||||
step_cache_id: Step cache ID for seeding
|
||||
|
||||
Returns:
|
||||
SandboxResult
|
||||
"""
|
||||
# Check effect metadata for trust level override
|
||||
metadata = self.cache.get_effect_metadata(effect_hash)
|
||||
trust_level = self.trust_level
|
||||
if metadata:
|
||||
# L1 owner can mark effect as trusted
|
||||
if metadata.get("trust_level") == "trusted":
|
||||
trust_level = "trusted"
|
||||
|
||||
return run_effect_from_cache(
|
||||
cache=self.cache,
|
||||
effect_hash=effect_hash,
|
||||
input_paths=input_paths,
|
||||
output_path=output_path,
|
||||
params=params,
|
||||
analysis=analysis,
|
||||
cache_id=step_cache_id,
|
||||
trust_level=trust_level,
|
||||
)
|
||||
|
||||
def is_temporal(self, effect_hash: str) -> bool:
|
||||
"""Check if effect is temporal."""
|
||||
return check_effect_temporal(self.cache, effect_hash)
|
||||
|
||||
def get_api_type(self, effect_hash: str) -> str:
|
||||
"""Get effect API type."""
|
||||
return get_effect_api_type(self.cache, effect_hash)
|
||||
Reference in New Issue
Block a user