Files
script.service.ultimate/lib/streaming_providers/base/settings/models/settings_models.py
T
2025-10-29 20:23:50 +01:00

567 lines
20 KiB
Python

# streaming_providers/base/settings/models/settings_models.py
from dataclasses import dataclass, field
from typing import Any, Optional, Callable, Union, List, Dict
from enum import Enum
import re
import ipaddress
from urllib.parse import urlparse
class SettingType(Enum):
"""Supported setting value types"""
STRING = "string"
INTEGER = "integer"
BOOLEAN = "boolean"
FLOAT = "float"
SELECT = "select" # dropdown with predefined options
PASSWORD = "password" # masked string input
URL = "url" # URL with validation
IP_ADDRESS = "ip_address" # IP address validation
PORT = "port" # Port number (1-65535)
EMAIL = "email" # Email address validation
@dataclass
class ValidationRule:
"""Validation rule for a setting value"""
name: str
validator: Callable[[Any], bool]
error_message: str
def validate(self, value: Any) -> bool:
"""Validate a setting value against this rule"""
try:
return self.validator(value)
except Exception:
return False
def get_error_message(self) -> str:
"""Get human-readable error message for validation failure"""
return self.error_message
class StandardValidationRules:
"""Standard validation rules for common use cases"""
@staticmethod
def not_empty(error_msg: str = "Value cannot be empty") -> ValidationRule:
"""Rule to ensure value is not empty"""
return ValidationRule(
name="not_empty",
validator=lambda x: x is not None and str(x).strip() != "",
error_message=error_msg
)
@staticmethod
def min_length(min_len: int, error_msg: Optional[str] = None) -> ValidationRule:
"""Rule to ensure string has minimum length"""
if error_msg is None:
error_msg = f"Value must be at least {min_len} characters long"
return ValidationRule(
name="min_length",
validator=lambda x: isinstance(x, str) and len(x) >= min_len,
error_message=error_msg
)
@staticmethod
def max_length(max_len: int, error_msg: Optional[str] = None) -> ValidationRule:
"""Rule to ensure string has maximum length"""
if error_msg is None:
error_msg = f"Value must be at most {max_len} characters long"
return ValidationRule(
name="max_length",
validator=lambda x: isinstance(x, str) and len(x) <= max_len,
error_message=error_msg
)
@staticmethod
def numeric_range(min_val: Union[int, float], max_val: Union[int, float],
error_msg: Optional[str] = None) -> ValidationRule:
"""Rule to ensure numeric value is within range"""
if error_msg is None:
error_msg = f"Value must be between {min_val} and {max_val}"
return ValidationRule(
name="numeric_range",
validator=lambda x: isinstance(x, (int, float)) and min_val <= x <= max_val,
error_message=error_msg
)
@staticmethod
def regex_pattern(pattern: str, error_msg: str) -> ValidationRule:
"""Rule to validate against regex pattern"""
compiled_pattern = re.compile(pattern)
return ValidationRule(
name="regex_pattern",
validator=lambda x: isinstance(x, str) and bool(compiled_pattern.match(x)),
error_message=error_msg
)
@staticmethod
def valid_email(error_msg: str = "Invalid email address format") -> ValidationRule:
"""Rule to validate email address"""
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return StandardValidationRules.regex_pattern(email_pattern, error_msg)
@staticmethod
def valid_url(error_msg: str = "Invalid URL format") -> ValidationRule:
"""Rule to validate URL format"""
def validate_url(value: str) -> bool:
if not isinstance(value, str):
return False
try:
result = urlparse(value)
return all([result.scheme, result.netloc])
except Exception:
return False
return ValidationRule(
name="valid_url",
validator=validate_url,
error_message=error_msg
)
@staticmethod
def valid_ip_address(error_msg: str = "Invalid IP address format") -> ValidationRule:
"""Rule to validate IP address (IPv4 or IPv6)"""
def validate_ip(value: str) -> bool:
if not isinstance(value, str):
return False
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False
return ValidationRule(
name="valid_ip_address",
validator=validate_ip,
error_message=error_msg
)
@staticmethod
def valid_port(error_msg: str = "Port must be between 1 and 65535") -> ValidationRule:
"""Rule to validate port number"""
return ValidationRule(
name="valid_port",
validator=lambda x: isinstance(x, int) and 1 <= x <= 65535,
error_message=error_msg
)
@staticmethod
def in_choices(choices: List[Any], error_msg: Optional[str] = None) -> ValidationRule:
"""Rule to ensure value is in predefined choices"""
if error_msg is None:
error_msg = f"Value must be one of: {', '.join(map(str, choices))}"
return ValidationRule(
name="in_choices",
validator=lambda x: x in choices,
error_message=error_msg
)
@dataclass
class SettingValue:
"""Container for a setting value with metadata and validation"""
setting_type: SettingType
default_value: Any = None
current_value: Any = None
choices: Optional[List[Any]] = None # For SELECT type
validation_rules: List[ValidationRule] = field(default_factory=list)
description: Optional[str] = None
display_name: Optional[str] = None
is_sensitive: bool = False # For passwords, secrets, etc.
kodi_setting_id: Optional[str] = None # Mapping to Kodi setting ID
def __post_init__(self):
"""Post-initialization setup"""
# Set current value to default if not provided
if self.current_value is None:
self.current_value = self.default_value
# Add type-specific validation rules
self._add_type_validation()
# Add choices validation for SELECT type
if self.setting_type == SettingType.SELECT and self.choices:
self.add_validation_rule(
StandardValidationRules.in_choices(self.choices)
)
def is_required(self) -> bool:
"""Check if this setting is required (has not_empty validation rule)"""
return any(rule.name == "not_empty" for rule in self.validation_rules)
def _add_type_validation(self):
"""Add validation rules based on setting type"""
if self.setting_type == SettingType.INTEGER:
self.add_validation_rule(ValidationRule(
name="is_integer",
validator=lambda x: isinstance(x, int) or (isinstance(x, str) and x.isdigit()),
error_message="Value must be an integer"
))
elif self.setting_type == SettingType.FLOAT:
self.add_validation_rule(ValidationRule(
name="is_float",
validator=lambda x: isinstance(x, (int, float)) or self._is_valid_float_string(x),
error_message="Value must be a number"
))
elif self.setting_type == SettingType.BOOLEAN:
self.add_validation_rule(ValidationRule(
name="is_boolean",
validator=lambda x: isinstance(x, bool) or str(x).lower() in ['true', 'false', '1', '0'],
error_message="Value must be true or false"
))
elif self.setting_type == SettingType.URL:
self.add_validation_rule(StandardValidationRules.valid_url())
elif self.setting_type == SettingType.IP_ADDRESS:
self.add_validation_rule(StandardValidationRules.valid_ip_address())
elif self.setting_type == SettingType.PORT:
self.add_validation_rule(StandardValidationRules.valid_port())
elif self.setting_type == SettingType.EMAIL:
self.add_validation_rule(StandardValidationRules.valid_email())
def _is_valid_float_string(self, value: Any) -> bool:
"""Check if string can be converted to float"""
if not isinstance(value, str):
return False
try:
float(value)
return True
except ValueError:
return False
def set_value(self, value: Any) -> bool:
"""
Set the setting value with validation and type conversion
Args:
value: New value to set
Returns:
True if value was set successfully, False if validation failed
"""
# Convert value to appropriate type
converted_value = self._convert_value(value)
if converted_value is None:
return False
# Validate the converted value
if not self._validate_value(converted_value):
return False
self.current_value = converted_value
return True
def _convert_value(self, value: Any) -> Any:
"""Convert value to the appropriate type"""
if value is None:
return None
try:
if self.setting_type == SettingType.STRING or self.setting_type == SettingType.PASSWORD:
return str(value)
elif self.setting_type == SettingType.INTEGER or self.setting_type == SettingType.PORT:
if isinstance(value, int):
return value
elif isinstance(value, str) and value.isdigit():
return int(value)
else:
return None
elif self.setting_type == SettingType.FLOAT:
if isinstance(value, (int, float)):
return float(value)
elif isinstance(value, str):
return float(value)
else:
return None
elif self.setting_type == SettingType.BOOLEAN:
if isinstance(value, bool):
return value
elif isinstance(value, str):
return value.lower() in ['true', '1', 'yes', 'on']
elif isinstance(value, int):
return bool(value)
else:
return None
elif self.setting_type in [SettingType.SELECT, SettingType.URL,
SettingType.IP_ADDRESS, SettingType.EMAIL]:
return str(value)
else:
return value
except (ValueError, TypeError):
return None
def _validate_value(self, value: Any) -> bool:
"""Validate value against all validation rules"""
for rule in self.validation_rules:
if not rule.validate(value):
return False
return True
def get_value(self) -> Any:
"""Get the current setting value"""
return self.current_value
def get_display_value(self) -> str:
"""Get value formatted for display (masks sensitive values)"""
if self.is_sensitive and self.current_value:
return "*" * min(len(str(self.current_value)), 8)
return str(self.current_value) if self.current_value is not None else ""
def add_validation_rule(self, rule: ValidationRule) -> None:
"""Add a validation rule to this setting"""
# Check if rule with same name already exists
existing_names = {r.name for r in self.validation_rules}
if rule.name not in existing_names:
self.validation_rules.append(rule)
def remove_validation_rule(self, rule_name: str) -> bool:
"""Remove a validation rule by name"""
original_length = len(self.validation_rules)
self.validation_rules = [r for r in self.validation_rules if r.name != rule_name]
return len(self.validation_rules) < original_length
def validate(self) -> tuple[bool, List[str]]:
"""
Validate current value against all rules
Returns:
Tuple of (is_valid, list_of_error_messages)
"""
if self.current_value is None:
# Check if this setting is required (has not_empty rule)
has_required_rule = any(rule.name == "not_empty" for rule in self.validation_rules)
if has_required_rule:
return False, ["Value is required"]
else:
return True, []
errors = []
for rule in self.validation_rules:
if not rule.validate(self.current_value):
errors.append(rule.get_error_message())
return len(errors) == 0, errors
def reset_to_default(self) -> None:
"""Reset value to default"""
self.current_value = self.default_value
def has_value(self) -> bool:
"""Check if setting has a non-None value"""
return self.current_value is not None
def is_modified(self) -> bool:
"""Check if current value differs from default"""
return self.current_value != self.default_value
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary representation"""
is_valid, errors = self.validate()
return {
'setting_type': self.setting_type.value,
'current_value': self.current_value,
'default_value': self.default_value,
'choices': self.choices,
'description': self.description,
'display_name': self.display_name,
'is_sensitive': self.is_sensitive,
'kodi_setting_id': self.kodi_setting_id,
'is_valid': is_valid,
'validation_errors': errors,
'has_value': self.has_value(),
'is_modified': self.is_modified(),
'display_value': self.get_display_value()
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'SettingValue':
"""Create SettingValue from dictionary representation"""
setting = cls(
setting_type=SettingType(data['setting_type']),
default_value=data.get('default_value'),
current_value=data.get('current_value'),
choices=data.get('choices'),
description=data.get('description'),
display_name=data.get('display_name'),
is_sensitive=data.get('is_sensitive', False),
kodi_setting_id=data.get('kodi_setting_id')
)
return setting
def clone(self) -> 'SettingValue':
"""Create a copy of this setting"""
return SettingValue(
setting_type=self.setting_type,
default_value=self.default_value,
current_value=self.current_value,
choices=self.choices.copy() if self.choices else None,
validation_rules=self.validation_rules.copy(),
description=self.description,
display_name=self.display_name,
is_sensitive=self.is_sensitive,
kodi_setting_id=self.kodi_setting_id
)
class SettingValueBuilder:
"""Builder pattern for creating SettingValue instances"""
def __init__(self, setting_type: SettingType):
self._setting_type = setting_type
self._default_value = None
self._choices = None
self._validation_rules = []
self._description = None
self._display_name = None
self._is_sensitive = False
self._kodi_setting_id = None
def default(self, value: Any) -> 'SettingValueBuilder':
"""Set default value"""
self._default_value = value
return self
def choices(self, choices: List[Any]) -> 'SettingValueBuilder':
"""Set choices for SELECT type"""
self._choices = choices
return self
def description(self, desc: str) -> 'SettingValueBuilder':
"""Set description"""
self._description = desc
return self
def display_name(self, name: str) -> 'SettingValueBuilder':
"""Set display name"""
self._display_name = name
return self
def sensitive(self, is_sensitive: bool = True) -> 'SettingValueBuilder':
"""Mark as sensitive (for passwords, etc.)"""
self._is_sensitive = is_sensitive
return self
def kodi_setting(self, setting_id: str) -> 'SettingValueBuilder':
"""Set Kodi setting ID mapping"""
self._kodi_setting_id = setting_id
return self
def required(self) -> 'SettingValueBuilder':
"""Mark as required (not empty)"""
self._validation_rules.append(StandardValidationRules.not_empty())
return self
def min_length(self, length: int) -> 'SettingValueBuilder':
"""Add minimum length validation"""
self._validation_rules.append(StandardValidationRules.min_length(length))
return self
def max_length(self, length: int) -> 'SettingValueBuilder':
"""Add maximum length validation"""
self._validation_rules.append(StandardValidationRules.max_length(length))
return self
def numeric_range(self, min_val: Union[int, float], max_val: Union[int, float]) -> 'SettingValueBuilder':
"""Add numeric range validation"""
self._validation_rules.append(StandardValidationRules.numeric_range(min_val, max_val))
return self
def custom_validation(self, rule: ValidationRule) -> 'SettingValueBuilder':
"""Add custom validation rule"""
self._validation_rules.append(rule)
return self
def build(self) -> SettingValue:
"""Build the SettingValue instance"""
setting = SettingValue(
setting_type=self._setting_type,
default_value=self._default_value,
choices=self._choices,
description=self._description,
display_name=self._display_name,
is_sensitive=self._is_sensitive,
kodi_setting_id=self._kodi_setting_id
)
# Add custom validation rules
for rule in self._validation_rules:
setting.add_validation_rule(rule)
return setting
# Convenience factory functions
def string_setting(default: str = "", required: bool = False) -> SettingValueBuilder:
"""Create a string setting builder"""
builder = SettingValueBuilder(SettingType.STRING).default(default)
if required:
builder.required()
return builder
def password_setting(required: bool = True) -> SettingValueBuilder:
"""Create a password setting builder"""
builder = SettingValueBuilder(SettingType.PASSWORD).sensitive(True)
if required:
builder.required()
return builder
def integer_setting(default: int = 0, min_val: Optional[int] = None,
max_val: Optional[int] = None) -> SettingValueBuilder:
"""Create an integer setting builder"""
builder = SettingValueBuilder(SettingType.INTEGER).default(default)
if min_val is not None and max_val is not None:
builder.numeric_range(min_val, max_val)
return builder
def boolean_setting(default: bool = False) -> SettingValueBuilder:
"""Create a boolean setting builder"""
return SettingValueBuilder(SettingType.BOOLEAN).default(default)
def select_setting(choices: List[Any], default: Any = None) -> SettingValueBuilder:
"""Create a select setting builder"""
builder = SettingValueBuilder(SettingType.SELECT).choices(choices)
if default is not None:
builder.default(default)
return builder
def url_setting(required: bool = False) -> SettingValueBuilder:
"""Create a URL setting builder"""
builder = SettingValueBuilder(SettingType.URL)
if required:
builder.required()
return builder
def port_setting(default: int = 8080) -> SettingValueBuilder:
"""Create a port setting builder"""
return SettingValueBuilder(SettingType.PORT).default(default)
def ip_setting(required: bool = False) -> SettingValueBuilder:
"""Create an IP address setting builder"""
builder = SettingValueBuilder(SettingType.IP_ADDRESS)
if required:
builder.required()
return builder