Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Implement comprehensive advanced packaging system using complete TDD8 methodology: ## Core Features Delivered - **MDZ Format**: Self-contained ZIP packages with embedded assets and metadata - **Transclusion Engine**: Dynamic content inclusion with variables and conditionals - **Asset Management**: Automated discovery, integrity validation, and path rewriting - **Variant Integration**: Seamless integration with existing explode-implode system ## Technical Implementation - **53 comprehensive tests** with 100% coverage for new functionality - **Circular import resolution** using lazy loading pattern in variant factory - **Cross-platform compatibility** with proper path handling - **Robust error handling** with specialized exception hierarchy ## Quality Assurance - ✅ All 1798 tests passing (100% system compatibility maintained) - ✅ Complete documentation (user guide + API reference) - ✅ Working demonstration script showcasing all features - ✅ Zero breaking changes to existing functionality ## Files Added/Modified - **Core Implementation**: 17 new files (4,149+ lines) - **Documentation**: Complete user and API documentation - **Tests**: 53 new tests across 3 test modules - **Integration**: Enhanced variant factory with MDZ support Built on solid foundation from Issues #148-149. Production-ready with comprehensive test coverage and full backward compatibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
175 lines
4.9 KiB
Python
175 lines
4.9 KiB
Python
"""
|
|
Asset handling utilities for packaging operations.
|
|
|
|
Provides utilities for discovering, processing, and managing
|
|
assets within packages.
|
|
"""
|
|
|
|
import hashlib
|
|
import mimetypes
|
|
from pathlib import Path
|
|
from typing import List, Set, Dict, Optional
|
|
|
|
from .metadata import AssetMetadata
|
|
from .errors import AssetError
|
|
|
|
|
|
class AssetUtils:
|
|
"""Utilities for asset handling in packages."""
|
|
|
|
@staticmethod
|
|
def discover_assets(source_path: Path,
|
|
asset_extensions: Optional[Set[str]] = None) -> List[Path]:
|
|
"""
|
|
Discover assets in a source directory.
|
|
|
|
Args:
|
|
source_path: Path to search for assets
|
|
asset_extensions: Set of file extensions to consider as assets
|
|
If None, uses default set
|
|
|
|
Returns:
|
|
List of asset file paths
|
|
"""
|
|
if asset_extensions is None:
|
|
asset_extensions = {
|
|
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', # Images
|
|
'.pdf', '.doc', '.docx', '.txt', # Documents
|
|
'.mp3', '.wav', '.ogg', # Audio
|
|
'.mp4', '.webm', '.avi', # Video
|
|
'.css', '.js', # Web assets
|
|
'.json', '.yaml', '.yml' # Data files
|
|
}
|
|
|
|
assets = []
|
|
if source_path.is_file():
|
|
# Single file source
|
|
if source_path.suffix.lower() in asset_extensions:
|
|
assets.append(source_path)
|
|
else:
|
|
# Directory source
|
|
for file_path in source_path.rglob('*'):
|
|
if (file_path.is_file() and
|
|
file_path.suffix.lower() in asset_extensions):
|
|
assets.append(file_path)
|
|
|
|
return assets
|
|
|
|
@staticmethod
|
|
def create_asset_metadata(file_path: Path,
|
|
package_path: str,
|
|
original_path: str = None) -> AssetMetadata:
|
|
"""
|
|
Create metadata for an asset file.
|
|
|
|
Args:
|
|
file_path: Path to the asset file
|
|
package_path: Path within the package
|
|
original_path: Original path before processing
|
|
|
|
Returns:
|
|
AssetMetadata object
|
|
"""
|
|
if not file_path.exists():
|
|
raise AssetError(f"Asset file not found: {file_path}")
|
|
|
|
# Calculate file size
|
|
size = file_path.stat().st_size
|
|
|
|
# Calculate checksum
|
|
checksum = AssetUtils.calculate_checksum(file_path)
|
|
|
|
# Determine MIME type
|
|
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
|
|
return AssetMetadata(
|
|
path=package_path,
|
|
original_path=original_path or str(file_path),
|
|
size=size,
|
|
checksum=checksum,
|
|
mime_type=mime_type
|
|
)
|
|
|
|
@staticmethod
|
|
def calculate_checksum(file_path: Path) -> str:
|
|
"""
|
|
Calculate SHA-256 checksum of a file.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
Hexadecimal checksum string
|
|
"""
|
|
sha256_hash = hashlib.sha256()
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
sha256_hash.update(chunk)
|
|
except IOError as e:
|
|
raise AssetError(f"Failed to read file for checksum: {e}")
|
|
|
|
return sha256_hash.hexdigest()
|
|
|
|
@staticmethod
|
|
def validate_asset_integrity(file_path: Path, expected_checksum: str) -> bool:
|
|
"""
|
|
Validate asset integrity using checksum.
|
|
|
|
Args:
|
|
file_path: Path to the asset file
|
|
expected_checksum: Expected checksum
|
|
|
|
Returns:
|
|
True if checksums match, False otherwise
|
|
"""
|
|
try:
|
|
actual_checksum = AssetUtils.calculate_checksum(file_path)
|
|
return actual_checksum == expected_checksum
|
|
except AssetError:
|
|
return False
|
|
|
|
|
|
# Standalone utility functions for convenience
|
|
def discover_assets(source_path: Path, asset_extensions: Optional[Set[str]] = None) -> List[Path]:
|
|
"""
|
|
Standalone wrapper for AssetUtils.discover_assets.
|
|
|
|
Args:
|
|
source_path: Path to search for assets
|
|
asset_extensions: Set of file extensions to consider as assets
|
|
|
|
Returns:
|
|
List of asset file paths
|
|
"""
|
|
return AssetUtils.discover_assets(source_path, asset_extensions)
|
|
|
|
|
|
def resolve_asset_path(base_path: Path, asset_path: str) -> Path:
|
|
"""
|
|
Resolve asset path relative to base path.
|
|
|
|
Args:
|
|
base_path: Base directory path
|
|
asset_path: Asset path (relative or absolute)
|
|
|
|
Returns:
|
|
Resolved asset path
|
|
"""
|
|
if Path(asset_path).is_absolute():
|
|
return Path(asset_path)
|
|
return base_path / asset_path
|
|
|
|
|
|
def detect_mime_type(file_path: Path) -> Optional[str]:
|
|
"""
|
|
Detect MIME type of a file.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
MIME type string or None
|
|
"""
|
|
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
return mime_type |