mirror of https://github.com/streamlink/streamlink
407 lines
11 KiB
Python
407 lines
11 KiB
Python
from collections import abc
|
|
from copy import copy, deepcopy
|
|
from functools import singledispatch
|
|
from re import Match, Pattern
|
|
|
|
from lxml.etree import Element, iselement
|
|
|
|
from streamlink.exceptions import PluginError
|
|
from streamlink.plugin.api.validate._exception import ValidationError
|
|
from streamlink.plugin.api.validate._schemas import (
|
|
AllSchema,
|
|
AnySchema,
|
|
AttrSchema,
|
|
GetItemSchema,
|
|
ListSchema,
|
|
NoneOrAllSchema,
|
|
OptionalSchema,
|
|
RegexSchema,
|
|
TransformSchema,
|
|
UnionGetSchema,
|
|
UnionSchema,
|
|
XmlElementSchema,
|
|
)
|
|
|
|
|
|
class Schema(AllSchema):
|
|
"""
|
|
Wrapper class for :class:`AllSchema` with a validate method which raises :class:`PluginError` by default on error.
|
|
"""
|
|
|
|
def validate(self, value, name="result", exception=PluginError):
|
|
try:
|
|
return validate(self, value)
|
|
except ValidationError as err:
|
|
raise exception(f"Unable to validate {name}: {err}") from None
|
|
|
|
|
|
# ----
|
|
|
|
|
|
@singledispatch
|
|
def validate(schema, value):
|
|
if schema != value:
|
|
raise ValidationError(
|
|
"{value} does not equal {expected}",
|
|
value=repr(value),
|
|
expected=repr(schema),
|
|
schema="equality",
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
@validate.register(type)
|
|
def _validate_type(schema, value):
|
|
if not isinstance(value, schema):
|
|
raise ValidationError(
|
|
"Type of {value} should be {expected}, but is {actual}",
|
|
value=repr(value),
|
|
expected=schema.__name__,
|
|
actual=type(value).__name__,
|
|
schema=type,
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
@validate.register(list)
|
|
@validate.register(tuple)
|
|
@validate.register(set)
|
|
@validate.register(frozenset)
|
|
def _validate_sequence(schema, value):
|
|
cls = type(schema)
|
|
validate(cls, value)
|
|
|
|
return cls(
|
|
validate(AnySchema(*schema), v) for v in value
|
|
)
|
|
|
|
|
|
@validate.register(dict)
|
|
def _validate_dict(schema, value):
|
|
cls = type(schema)
|
|
validate(cls, value)
|
|
new = cls()
|
|
|
|
for key, subschema in schema.items():
|
|
if isinstance(key, OptionalSchema):
|
|
if key.key not in value:
|
|
continue
|
|
key = key.key
|
|
|
|
if type(key) in (type, AllSchema, AnySchema, TransformSchema, UnionSchema):
|
|
for subkey, subvalue in value.items():
|
|
try:
|
|
newkey = validate(key, subkey)
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate key", schema=dict) from err
|
|
try:
|
|
newvalue = validate(subschema, subvalue)
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate value", schema=dict) from err
|
|
new[newkey] = newvalue
|
|
break
|
|
|
|
if key not in value:
|
|
raise ValidationError(
|
|
"Key {key} not found in {value}",
|
|
key=repr(key),
|
|
value=repr(value),
|
|
schema=dict,
|
|
)
|
|
|
|
try:
|
|
new[key] = validate(subschema, value[key])
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate value of key {key}", key=repr(key), schema=dict) from err
|
|
|
|
return new
|
|
|
|
|
|
@validate.register
|
|
def _validate_callable(schema: abc.Callable, value):
|
|
if not schema(value):
|
|
raise ValidationError(
|
|
"{callable} is not true",
|
|
callable=f"{schema.__name__}({value!r})",
|
|
schema=abc.Callable,
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
@validate.register
|
|
def _validate_pattern(schema: Pattern, value):
|
|
if not isinstance(value, (str, bytes)):
|
|
raise ValidationError(
|
|
"Type of {value} should be str or bytes, but is {actual}",
|
|
value=repr(value),
|
|
actual=type(value).__name__,
|
|
schema=Pattern,
|
|
)
|
|
|
|
try:
|
|
result = schema.search(value)
|
|
except TypeError as err:
|
|
raise ValidationError(err, schema=Pattern) from None
|
|
|
|
return result
|
|
|
|
|
|
@validate.register
|
|
def _validate_allschema(schema: AllSchema, value):
|
|
for subschema in schema.schema:
|
|
value = validate(subschema, value)
|
|
|
|
return value
|
|
|
|
|
|
@validate.register
|
|
def _validate_anyschema(schema: AnySchema, value):
|
|
errors = []
|
|
for subschema in schema.schema:
|
|
try:
|
|
return validate(subschema, value)
|
|
except ValidationError as err:
|
|
errors.append(err)
|
|
|
|
raise ValidationError(*errors, schema=AnySchema)
|
|
|
|
|
|
@validate.register
|
|
def _validate_noneorallschema(schema: NoneOrAllSchema, value):
|
|
if value is not None:
|
|
try:
|
|
for subschema in schema.schema:
|
|
value = validate(subschema, value)
|
|
except ValidationError as err:
|
|
raise ValidationError(err, schema=NoneOrAllSchema) from None
|
|
|
|
return value
|
|
|
|
|
|
@validate.register
|
|
def _validate_listschema(schema: ListSchema, value):
|
|
if type(value) is not list:
|
|
raise ValidationError(
|
|
"Type of {value} should be list, but is {actual}",
|
|
value=repr(value),
|
|
actual=type(value).__name__,
|
|
schema=ListSchema,
|
|
)
|
|
if len(value) != len(schema.schema):
|
|
raise ValidationError(
|
|
"Length of list ({length}) does not match expectation ({expected})",
|
|
length=len(value),
|
|
expected=len(schema.schema),
|
|
schema=ListSchema,
|
|
)
|
|
|
|
new = []
|
|
errors = []
|
|
for k, v in enumerate(schema.schema):
|
|
try:
|
|
new.append(validate(v, value[k]))
|
|
except ValidationError as err:
|
|
errors.append(err)
|
|
|
|
if errors:
|
|
raise ValidationError(*errors, schema=ListSchema)
|
|
|
|
return new
|
|
|
|
|
|
@validate.register
|
|
def _validate_regexschema(schema: RegexSchema, value):
|
|
if not isinstance(value, (str, bytes)):
|
|
raise ValidationError(
|
|
"Type of {value} should be str or bytes, but is {actual}",
|
|
value=repr(value),
|
|
actual=type(value).__name__,
|
|
schema=RegexSchema,
|
|
)
|
|
|
|
try:
|
|
result = getattr(schema.pattern, schema.method)(value)
|
|
except TypeError as err:
|
|
raise ValidationError(err, schema=RegexSchema) from None
|
|
|
|
if result is None:
|
|
raise ValidationError(
|
|
"Pattern {pattern} did not match {value}",
|
|
pattern=repr(schema.pattern.pattern),
|
|
value=repr(value),
|
|
schema=RegexSchema,
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
@validate.register
|
|
def _validate_transformschema(schema: TransformSchema, value):
|
|
validate(abc.Callable, schema.func)
|
|
return schema.func(value, *schema.args, **schema.kwargs)
|
|
|
|
|
|
@validate.register
|
|
def _validate_getitemschema(schema: GetItemSchema, value):
|
|
item = schema.item if type(schema.item) is tuple and not schema.strict else (schema.item,)
|
|
idx = 0
|
|
key = None
|
|
try:
|
|
for key in item:
|
|
if iselement(value):
|
|
value = value.attrib[key]
|
|
elif isinstance(value, Match):
|
|
value = value.group(key)
|
|
else:
|
|
value = value[key]
|
|
idx += 1
|
|
return value
|
|
except (KeyError, IndexError):
|
|
# only return default value on last item in nested lookup
|
|
if idx < len(item) - 1:
|
|
raise ValidationError(
|
|
"Item {key} was not found in object {value}",
|
|
key=repr(key),
|
|
value=repr(value),
|
|
schema=GetItemSchema,
|
|
) from None
|
|
return schema.default
|
|
except (TypeError, AttributeError) as err:
|
|
raise ValidationError(
|
|
"Could not get key {key} from object {value}",
|
|
key=repr(key),
|
|
value=repr(value),
|
|
schema=GetItemSchema,
|
|
) from err
|
|
|
|
|
|
@validate.register
|
|
def _validate_attrschema(schema: AttrSchema, value):
|
|
new = copy(value)
|
|
|
|
for key, subschema in schema.schema.items():
|
|
if not hasattr(value, key):
|
|
raise ValidationError(
|
|
"Attribute {key} not found on object {value}",
|
|
key=repr(key),
|
|
value=repr(value),
|
|
schema=AttrSchema,
|
|
)
|
|
|
|
try:
|
|
value = validate(subschema, getattr(value, key))
|
|
except ValidationError as err:
|
|
raise ValidationError(
|
|
"Could not validate attribute {key}",
|
|
key=repr(key),
|
|
schema=AttrSchema,
|
|
) from err
|
|
|
|
setattr(new, key, value)
|
|
|
|
return new
|
|
|
|
|
|
@validate.register
|
|
def _validate_xmlelementschema(schema: XmlElementSchema, value):
|
|
validate(iselement, value)
|
|
tag = value.tag
|
|
attrib = value.attrib
|
|
text = value.text
|
|
tail = value.tail
|
|
|
|
if schema.tag is not None:
|
|
try:
|
|
tag = validate(schema.tag, value.tag)
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate XML tag", schema=XmlElementSchema) from err
|
|
|
|
if schema.attrib is not None:
|
|
try:
|
|
attrib = validate(schema.attrib, dict(value.attrib))
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate XML attributes", schema=XmlElementSchema) from err
|
|
|
|
if schema.text is not None:
|
|
try:
|
|
text = validate(schema.text, value.text)
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate XML text", schema=XmlElementSchema) from err
|
|
|
|
if schema.tail is not None:
|
|
try:
|
|
tail = validate(schema.tail, value.tail)
|
|
except ValidationError as err:
|
|
raise ValidationError("Unable to validate XML tail", schema=XmlElementSchema) from err
|
|
|
|
new = Element(tag, attrib)
|
|
new.text = text
|
|
new.tail = tail
|
|
for child in value:
|
|
new.append(deepcopy(child))
|
|
|
|
return new
|
|
|
|
|
|
@validate.register
|
|
def _validate_uniongetschema(schema: UnionGetSchema, value):
|
|
return schema.seq(
|
|
validate(getter, value) for getter in schema.getters
|
|
)
|
|
|
|
|
|
@validate.register
|
|
def _validate_unionschema(schema: UnionSchema, value):
|
|
try:
|
|
return validate_union(schema.schema, value)
|
|
except ValidationError as err:
|
|
raise ValidationError("Could not validate union", schema=UnionSchema) from err
|
|
|
|
|
|
# ----
|
|
|
|
|
|
# noinspection PyUnusedLocal
|
|
@singledispatch
|
|
def validate_union(schema, value):
|
|
raise ValidationError(
|
|
"Invalid union type: {type}",
|
|
type=type(schema).__name__,
|
|
)
|
|
|
|
|
|
@validate_union.register(dict)
|
|
def _validate_union_dict(schema, value):
|
|
new = type(schema)()
|
|
for key, subschema in schema.items():
|
|
is_optional = isinstance(key, OptionalSchema)
|
|
if is_optional:
|
|
key = key.key
|
|
|
|
try:
|
|
new[key] = validate(subschema, value)
|
|
except ValidationError as err:
|
|
if is_optional:
|
|
continue
|
|
|
|
raise ValidationError(
|
|
"Unable to validate union {key}",
|
|
key=repr(key),
|
|
schema=dict,
|
|
) from err
|
|
|
|
return new
|
|
|
|
|
|
@validate_union.register(list)
|
|
@validate_union.register(tuple)
|
|
@validate_union.register(set)
|
|
@validate_union.register(frozenset)
|
|
def _validate_union_sequence(schemas, value):
|
|
return type(schemas)(
|
|
validate(schema, value) for schema in schemas
|
|
)
|