Add testing infrastructure and refactor DAG transformation
Testing setup: - Add pyproject.toml with mypy and pytest configuration - Add requirements-dev.txt for development dependencies - Create tests/ directory with test fixtures - Add 17 unit tests for DAG transformation pipeline Type annotations: - Add app/types.py with TypedDict definitions for node configs - Add typed helper functions: transform_node, build_input_name_mapping, bind_inputs, prepare_dag_for_execution - Refactor run_recipe to use the new typed helpers Regression tests for today's bugs: - test_effect_cid_key_not_effect_hash: Verifies CID uses 'cid' key - test_source_cid_binding_persists: Verifies bound CIDs in final DAG Run tests with: pytest tests/ -v Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
160
app/types.py
Normal file
160
app/types.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Type definitions for Art DAG L1 server.
|
||||
|
||||
Uses TypedDict for configuration structures to enable mypy checking.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, TypedDict, Union
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
# === Node Config Types ===
|
||||
|
||||
class SourceConfig(TypedDict, total=False):
|
||||
"""Config for SOURCE nodes."""
|
||||
cid: str # Content ID (IPFS CID or SHA3-256 hash)
|
||||
asset: str # Asset name from registry
|
||||
input: bool # True if this is a variable input
|
||||
name: str # Human-readable name for variable inputs
|
||||
description: str # Description for variable inputs
|
||||
|
||||
|
||||
class EffectConfig(TypedDict, total=False):
|
||||
"""Config for EFFECT nodes."""
|
||||
effect: str # Effect name
|
||||
cid: str # Effect CID (for cached/IPFS effects)
|
||||
# Effect parameters are additional keys
|
||||
intensity: float
|
||||
level: float
|
||||
|
||||
|
||||
class SequenceConfig(TypedDict, total=False):
|
||||
"""Config for SEQUENCE nodes."""
|
||||
transition: Dict[str, Any] # Transition config
|
||||
|
||||
|
||||
class SegmentConfig(TypedDict, total=False):
|
||||
"""Config for SEGMENT nodes."""
|
||||
start: float
|
||||
end: float
|
||||
duration: float
|
||||
|
||||
|
||||
# Union of all config types
|
||||
NodeConfig = Union[SourceConfig, EffectConfig, SequenceConfig, SegmentConfig, Dict[str, Any]]
|
||||
|
||||
|
||||
# === Node Types ===
|
||||
|
||||
class CompiledNode(TypedDict):
|
||||
"""Node as produced by the S-expression compiler."""
|
||||
id: str
|
||||
type: str # "SOURCE", "EFFECT", "SEQUENCE", etc.
|
||||
config: Dict[str, Any]
|
||||
inputs: List[str]
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
class TransformedNode(TypedDict):
|
||||
"""Node after transformation for artdag execution."""
|
||||
node_id: str
|
||||
node_type: str
|
||||
config: Dict[str, Any]
|
||||
inputs: List[str]
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
# === DAG Types ===
|
||||
|
||||
class CompiledDAG(TypedDict):
|
||||
"""DAG as produced by the S-expression compiler."""
|
||||
nodes: List[CompiledNode]
|
||||
output: str
|
||||
|
||||
|
||||
class TransformedDAG(TypedDict):
|
||||
"""DAG after transformation for artdag execution."""
|
||||
nodes: Dict[str, TransformedNode]
|
||||
output_id: str
|
||||
metadata: NotRequired[Dict[str, Any]]
|
||||
|
||||
|
||||
# === Registry Types ===
|
||||
|
||||
class AssetEntry(TypedDict, total=False):
|
||||
"""Asset in the recipe registry."""
|
||||
cid: str
|
||||
url: str
|
||||
|
||||
|
||||
class EffectEntry(TypedDict, total=False):
|
||||
"""Effect in the recipe registry."""
|
||||
cid: str
|
||||
url: str
|
||||
temporal: bool
|
||||
|
||||
|
||||
class Registry(TypedDict):
|
||||
"""Recipe registry containing assets and effects."""
|
||||
assets: Dict[str, AssetEntry]
|
||||
effects: Dict[str, EffectEntry]
|
||||
|
||||
|
||||
# === Recipe Types ===
|
||||
|
||||
class Recipe(TypedDict, total=False):
|
||||
"""Compiled recipe structure."""
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
owner: str
|
||||
registry: Registry
|
||||
dag: CompiledDAG
|
||||
recipe_id: str
|
||||
ipfs_cid: str
|
||||
sexp: str
|
||||
step_count: int
|
||||
error: str
|
||||
|
||||
|
||||
# === API Request/Response Types ===
|
||||
|
||||
class RecipeRunInputs(TypedDict):
|
||||
"""Mapping of input names to CIDs for recipe execution."""
|
||||
# Keys are input names, values are CIDs
|
||||
pass # Actually just Dict[str, str]
|
||||
|
||||
|
||||
class RunResult(TypedDict, total=False):
|
||||
"""Result of a recipe run."""
|
||||
run_id: str
|
||||
status: str # "pending", "running", "completed", "failed"
|
||||
recipe: str
|
||||
inputs: List[str]
|
||||
output_cid: str
|
||||
ipfs_cid: str
|
||||
error: str
|
||||
created_at: str
|
||||
completed_at: str
|
||||
|
||||
|
||||
# === Helper functions for type narrowing ===
|
||||
|
||||
def is_source_node(node: TransformedNode) -> bool:
|
||||
"""Check if node is a SOURCE node."""
|
||||
return node.get("node_type") == "SOURCE"
|
||||
|
||||
|
||||
def is_effect_node(node: TransformedNode) -> bool:
|
||||
"""Check if node is an EFFECT node."""
|
||||
return node.get("node_type") == "EFFECT"
|
||||
|
||||
|
||||
def is_variable_input(config: Dict[str, Any]) -> bool:
|
||||
"""Check if a SOURCE node config represents a variable input."""
|
||||
return bool(config.get("input"))
|
||||
|
||||
|
||||
def get_effect_cid(config: Dict[str, Any]) -> Optional[str]:
|
||||
"""Get effect CID from config, checking both 'cid' and 'hash' keys."""
|
||||
return config.get("cid") or config.get("hash")
|
||||
Reference in New Issue
Block a user