mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-18 07:02:27 +02:00
582 lines
20 KiB
Python
582 lines
20 KiB
Python
# streaming_providers/base/settings/models/settings_models.py
|
|
import ipaddress
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
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
|