feat: Implement Sideline plugin system with consistent terminology

This commit implements the Sideline/Mainline split with a clean plugin architecture:

## Core Changes

### Sideline Framework (New Directory)
- Created  directory containing the reusable pipeline framework
- Moved pipeline core, controllers, adapters, and registry to
- Moved display system to
- Moved effects system to
- Created plugin system with security and compatibility management in
- Created preset pack system with ASCII art encoding in
- Added default font (Corptic) to
- Added terminal ANSI constants to

### Mainline Application (Updated)
- Created  for Mainline stage component registration
- Updated  to register Mainline stages at startup
- Updated  as a compatibility shim re-exporting from sideline

### Terminology Consistency
- : Base class for all pipeline components (sources, effects, displays, cameras)
- : Base class for distributable plugin packages (was )
- : Base class for visual effects (was )
- Backward compatibility aliases maintained for existing code

## Key Features
- Plugin discovery via entry points and explicit registration
- Security permissions system for plugins
- Compatibility management with semantic version constraints
- Preset pack system for distributable configurations
- Default font bundled with Sideline (Corptic.otf)

## Testing
- Updated tests to register Mainline stages before discovery
- All StageRegistry tests passing

Note: This is a major refactoring that separates the framework (Sideline) from the application (Mainline), enabling Sideline to be used by other applications.
This commit is contained in:
2026-03-23 20:42:33 -07:00
parent 2d28e92594
commit e4b143ff36
58 changed files with 10163 additions and 50 deletions

View File

@@ -0,0 +1,26 @@
"""
Sideline Plugin System.
This module provides the plugin framework for Sideline, allowing applications
to extend the pipeline with custom stages, effects, and sources.
Features:
- Plugin base classes with metadata
- Security permission system
- Compatibility management
- Entry point discovery
"""
from sideline.plugins.base import StagePlugin, Plugin, PluginMetadata
from sideline.plugins.security import SecurityCapability, SecurityManager
from sideline.plugins.compatibility import VersionConstraint, CompatibilityManager
__all__ = [
"StagePlugin",
"Plugin", # Backward compatibility alias
"PluginMetadata",
"SecurityCapability",
"SecurityManager",
"VersionConstraint",
"CompatibilityManager",
]

78
sideline/plugins/base.py Normal file
View File

@@ -0,0 +1,78 @@
"""
Base classes for Sideline plugins.
Provides Plugin base class and PluginMetadata for plugin registration.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import ClassVar, Set
@dataclass
class PluginMetadata:
"""Plugin metadata with security and compatibility information."""
name: str
version: str
author: str
description: str
sideline_version: str # Compatible Sideline version (semver constraint)
permissions: Set[str] = field(default_factory=set) # Required security permissions
capabilities: Set[str] = field(default_factory=set) # Provided capabilities
def validate(self) -> None:
"""Validate metadata fields."""
if not self.name:
raise ValueError("Plugin name cannot be empty")
if not self.version:
raise ValueError("Plugin version cannot be empty")
if not self.author:
raise ValueError("Plugin author cannot be empty")
if not self.sideline_version:
raise ValueError("Plugin sideline_version cannot be empty")
class StagePlugin(ABC):
"""Base class for Sideline stage plugins (distributable pipeline components).
A StagePlugin represents a distributable unit that can contain one or more
pipeline stages. Plugins provide metadata for security, compatibility,
and versioning.
Subclasses must implement:
- validate_security(granted_permissions) -> bool
"""
metadata: ClassVar[PluginMetadata]
@abstractmethod
def validate_security(self, granted_permissions: Set[str]) -> bool:
"""Check if plugin has required permissions.
Args:
granted_permissions: Set of granted security permissions
Returns:
True if plugin has all required permissions
"""
pass
@classmethod
def get_metadata(cls) -> PluginMetadata:
"""Get plugin metadata."""
return cls.metadata
@classmethod
def get_required_permissions(cls) -> Set[str]:
"""Get required security permissions."""
return cls.metadata.permissions
@classmethod
def get_provided_capabilities(cls) -> Set[str]:
"""Get provided capabilities."""
return cls.metadata.capabilities
# Backward compatibility alias
Plugin = StagePlugin

View File

@@ -0,0 +1,259 @@
"""
Compatibility management for Sideline plugins.
Provides semantic version constraint checking and validation.
"""
import re
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
class Version:
"""Semantic version representation."""
major: int
minor: int
patch: int
pre_release: Optional[str] = None
build_metadata: Optional[str] = None
@classmethod
def parse(cls, version_str: str) -> "Version":
"""Parse version string into Version object.
Supports formats like:
- 1.2.3
- 1.2.3-alpha
- 1.2.3-beta.1
- 1.2.3+build.123
"""
# Remove build metadata if present
if "+" in version_str:
version_str, build_metadata = version_str.split("+", 1)
else:
build_metadata = None
# Parse pre-release if present
pre_release = None
if "-" in version_str:
version_str, pre_release = version_str.split("-", 1)
# Parse major.minor.patch
parts = version_str.split(".")
if len(parts) != 3:
raise ValueError(f"Invalid version format: {version_str}")
try:
major = int(parts[0])
minor = int(parts[1])
patch = int(parts[2])
except ValueError:
raise ValueError(f"Invalid version numbers: {version_str}")
return cls(major, minor, patch, pre_release, build_metadata)
def __str__(self) -> str:
result = f"{self.major}.{self.minor}.{self.patch}"
if self.pre_release:
result += f"-{self.pre_release}"
if self.build_metadata:
result += f"+{self.build_metadata}"
return result
def __lt__(self, other: "Version") -> bool:
if not isinstance(other, Version):
return NotImplemented
# Compare major.minor.patch
if (self.major, self.minor, self.patch) < (
other.major,
other.minor,
other.patch,
):
return True
if (self.major, self.minor, self.patch) > (
other.major,
other.minor,
other.patch,
):
return False
# Pre-release versions have lower precedence
if self.pre_release and not other.pre_release:
return True
if not self.pre_release and other.pre_release:
return False
if self.pre_release and other.pre_release:
return self.pre_release < other.pre_release
return False
def __le__(self, other: "Version") -> bool:
return self < other or self == other
def __gt__(self, other: "Version") -> bool:
return not (self <= other)
def __ge__(self, other: "Version") -> bool:
return not (self < other)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (
self.major == other.major
and self.minor == other.minor
and self.patch == other.patch
and self.pre_release == other.pre_release
and self.build_metadata == other.build_metadata
)
def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
class VersionConstraint:
"""Semantic version constraint parser and evaluator."""
def __init__(self, constraint: str):
"""Parse version constraint string.
Supports formats:
- "1.2.3" - exact version
- ">=1.2.3" - minimum version
- "<2.0.0" - maximum version
- ">=1.0.0,<2.0.0" - version range
- "~1.2.3" - pessimistic constraint (>=1.2.3,<1.3.0)
- "^1.2.3" - caret constraint (>=1.2.3,<2.0.0)
"""
self.constraint_str = constraint
self.min_version: Optional[Version] = None
self.max_version: Optional[Version] = None
self.exact_version: Optional[Version] = None
self._parse(constraint)
def _parse(self, constraint: str) -> None:
"""Parse constraint string."""
# Handle comma-separated constraints
if "," in constraint:
parts = [p.strip() for p in constraint.split(",")]
for part in parts:
self._parse_single(part)
else:
self._parse_single(constraint)
def _parse_single(self, constraint: str) -> None:
"""Parse a single constraint."""
constraint = constraint.strip()
# Exact version
if not any(op in constraint for op in [">=", "<=", ">", "<", "~", "^"]):
self.exact_version = Version.parse(constraint)
return
# Operator-based constraints
if ">=" in constraint:
op, version_str = constraint.split(">=", 1)
self.min_version = Version.parse(version_str.strip())
elif "<=" in constraint:
op, version_str = constraint.split("<=", 1)
self.max_version = Version.parse(version_str.strip())
elif ">" in constraint:
op, version_str = constraint.split(">", 1)
# Strict greater than - increment patch version
v = Version.parse(version_str.strip())
self.min_version = Version(v.major, v.minor, v.patch + 1)
elif "<" in constraint:
op, version_str = constraint.split("<", 1)
# Strict less than - decrement patch version (simplified)
v = Version.parse(version_str.strip())
self.max_version = Version(v.major, v.minor, v.patch - 1)
elif "~" in constraint:
# Pessimistic constraint: ~1.2.3 means >=1.2.3,<1.3.0
version_str = constraint[1:] # Remove ~
v = Version.parse(version_str.strip())
self.min_version = v
self.max_version = Version(v.major, v.minor + 1, 0)
elif "^" in constraint:
# Caret constraint: ^1.2.3 means >=1.2.3,<2.0.0
version_str = constraint[1:] # Remove ^
v = Version.parse(version_str.strip())
self.min_version = v
self.max_version = Version(v.major + 1, 0, 0)
def is_compatible(self, version: Version | str) -> bool:
"""Check if a version satisfies this constraint."""
if isinstance(version, str):
version = Version.parse(version)
# Exact version match
if self.exact_version is not None:
return version == self.exact_version
# Check minimum version
if self.min_version is not None:
if version < self.min_version:
return False
# Check maximum version
if self.max_version is not None:
if version >= self.max_version:
return False
return True
def __str__(self) -> str:
return self.constraint_str
class CompatibilityManager:
"""Manages plugin compatibility with Sideline."""
@classmethod
def get_sideline_version(cls) -> Version:
"""Get the current Sideline version."""
# Import here to avoid circular imports
import sideline
return Version.parse(sideline.__version__)
@classmethod
def validate_compatibility(cls, plugin_version_constraint: str) -> bool:
"""Validate plugin is compatible with current Sideline version.
Args:
plugin_version_constraint: Version constraint string from plugin metadata
Returns:
True if compatible, False otherwise
"""
try:
sideline_version = cls.get_sideline_version()
constraint = VersionConstraint(plugin_version_constraint)
return constraint.is_compatible(sideline_version)
except Exception as e:
# If parsing fails, consider incompatible
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Failed to validate compatibility: {e}")
return False
@classmethod
def get_compatibility_error(cls, plugin_version_constraint: str) -> Optional[str]:
"""Get compatibility error message if incompatible.
Returns:
Error message string or None if compatible
"""
if cls.validate_compatibility(plugin_version_constraint):
return None
sideline_version = cls.get_sideline_version()
return (
f"Plugin requires Sideline {plugin_version_constraint}, "
f"but current version is {sideline_version}"
)

View File

@@ -0,0 +1,92 @@
"""
Security system for Sideline plugins.
Provides permission-based security model for plugin execution.
"""
from enum import Enum, auto
from typing import Set
class SecurityCapability(Enum):
"""Security capability/permission definitions."""
READ = auto() # Read access to buffer/data
WRITE = auto() # Write access to buffer
NETWORK = auto() # Network access
FILESYSTEM = auto() # File system access
SYSTEM = auto() # System information access
def __str__(self) -> str:
return f"security.{self.name.lower()}"
@classmethod
def from_string(cls, permission: str) -> "SecurityCapability":
"""Parse security capability from string."""
if permission.startswith("security."):
permission = permission[9:] # Remove "security." prefix
try:
return cls[permission.upper()]
except KeyError:
raise ValueError(f"Unknown security capability: {permission}")
class SecurityManager:
"""Manages security permissions for plugin execution."""
def __init__(self):
self._granted_permissions: Set[str] = set()
def grant(self, permission: SecurityCapability | str) -> None:
"""Grant a security permission."""
if isinstance(permission, SecurityCapability):
permission = str(permission)
self._granted_permissions.add(permission)
def revoke(self, permission: SecurityCapability | str) -> None:
"""Revoke a security permission."""
if isinstance(permission, SecurityCapability):
permission = str(permission)
self._granted_permissions.discard(permission)
def has(self, permission: SecurityCapability | str) -> bool:
"""Check if a permission is granted."""
if isinstance(permission, SecurityCapability):
permission = str(permission)
return permission in self._granted_permissions
def has_all(self, permissions: Set[str]) -> bool:
"""Check if all permissions are granted."""
return all(self.has(p) for p in permissions)
def get_granted(self) -> Set[str]:
"""Get all granted permissions."""
return self._granted_permissions.copy()
def reset(self) -> None:
"""Reset all permissions."""
self._granted_permissions.clear()
# Global security manager instance
_global_security = SecurityManager()
def get_global_security() -> SecurityManager:
"""Get the global security manager instance."""
return _global_security
def grant(permission: SecurityCapability | str) -> None:
"""Grant a global security permission."""
_global_security.grant(permission)
def revoke(permission: SecurityCapability | str) -> None:
"""Revoke a global security permission."""
_global_security.revoke(permission)
def has(permission: SecurityCapability | str) -> bool:
"""Check if a global permission is granted."""
return _global_security.has(permission)