diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE b/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
index 54479cdbefd42..a061e74789b83 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
@@ -18,3 +18,7 @@ Parts of this code (src/main/python/) have been forked.
* License: Apache License 2.0
* Project: https://www.home-assistant.io/
* Source: https://github.com/home-assistant/core
+
+Parts of this code (src/main/python/voluptuous/) have been forked:
+* License: BSD 3-Clause
+* Source: https://github.com/alecthomas/voluptuous
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
index 514d5fdce1e6b..ebab6e560d3c1 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
@@ -15,7 +15,6 @@
openHAB Add-ons :: Bundles :: MQTT HomeAssistant Convention
- 24.2.1
bin/python3
@@ -38,7 +37,7 @@
org.openhab.osgiify
org.graalvm.polyglot.polyglot
- ${graalpy.version}
+ ${graalvm.version}
provided
@@ -49,7 +48,7 @@
org.graalvm.polyglot
python-community
- ${graalpy.version}
+ ${graalvm.version}
pom
provided
@@ -57,7 +56,7 @@
org.openhab.osgiify
org.graalvm.python.python-embedding
- ${graalpy.version}
+ ${graalvm.version}
provided
@@ -131,7 +130,7 @@
org.graalvm.python
graalpy-maven-plugin
- ${graalpy.version}
+ ${graalvm.version}
install-python-packages
@@ -144,7 +143,6 @@
awesomeversion==24.6.0
Jinja2==3.1.6
python-slugify==8.0.4
- voluptuous==0.15.2
@@ -160,7 +158,6 @@
awesomeversion==24.6.0
Jinja2==3.1.6
python-slugify==8.0.4
- voluptuous==0.15.2
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
index cc23d45f8bbdf..4121ef5cbba4f 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
@@ -7,24 +7,24 @@
openhab-runtime-base
openhab-transport-mqtt
openhab.tp-commons-net
- mvn:org.openhab.osgiify/org.graalvm.llvm.llvm-api/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-embedding/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-language/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-resources/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.regex.regex/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.collections/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.json/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-api/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-nfi/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/24.2.1
+ mvn:org.openhab.osgiify/org.graalvm.llvm.llvm-api/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-embedding/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-language/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-resources/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.regex.regex/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.collections/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.word/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.json/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-api/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-nfi/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/25.0.1
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/__init__.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/__init__.py
new file mode 100644
index 0000000000000..2891840e7e30a
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/__init__.py
@@ -0,0 +1,88 @@
+"""Schema validation for Python data structures.
+
+Given eg. a nested data structure like this:
+
+ {
+ 'exclude': ['Users', 'Uptime'],
+ 'include': [],
+ 'set': {
+ 'snmp_community': 'public',
+ 'snmp_timeout': 15,
+ 'snmp_version': '2c',
+ },
+ 'targets': {
+ 'localhost': {
+ 'exclude': ['Uptime'],
+ 'features': {
+ 'Uptime': {
+ 'retries': 3,
+ },
+ 'Users': {
+ 'snmp_community': 'monkey',
+ 'snmp_port': 15,
+ },
+ },
+ 'include': ['Users'],
+ 'set': {
+ 'snmp_community': 'monkeys',
+ },
+ },
+ },
+ }
+
+A schema like this:
+
+ >>> settings = {
+ ... 'snmp_community': str,
+ ... 'retries': int,
+ ... 'snmp_version': All(Coerce(str), Any('3', '2c', '1')),
+ ... }
+ >>> features = ['Ping', 'Uptime', 'Http']
+ >>> schema = Schema({
+ ... 'exclude': features,
+ ... 'include': features,
+ ... 'set': settings,
+ ... 'targets': {
+ ... 'exclude': features,
+ ... 'include': features,
+ ... 'features': {
+ ... str: settings,
+ ... },
+ ... },
+ ... })
+
+Validate like so:
+
+ >>> schema({
+ ... 'set': {
+ ... 'snmp_community': 'public',
+ ... 'snmp_version': '2c',
+ ... },
+ ... 'targets': {
+ ... 'exclude': ['Ping'],
+ ... 'features': {
+ ... 'Uptime': {'retries': 3},
+ ... 'Users': {'snmp_community': 'monkey'},
+ ... },
+ ... },
+ ... }) == {
+ ... 'set': {'snmp_version': '2c', 'snmp_community': 'public'},
+ ... 'targets': {
+ ... 'exclude': ['Ping'],
+ ... 'features': {'Uptime': {'retries': 3},
+ ... 'Users': {'snmp_community': 'monkey'}}}}
+ True
+"""
+
+# flake8: noqa
+# fmt: off
+from voluptuous.schema_builder import *
+from voluptuous.util import *
+from voluptuous.validators import *
+
+from voluptuous.error import * # isort: skip
+
+# fmt: on
+
+__version__ = '0.15.2'
+__author__ = 'alecthomas'
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/error.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/error.py
new file mode 100644
index 0000000000000..086afafcefd41
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/error.py
@@ -0,0 +1,219 @@
+# fmt: off
+import typing
+
+# fmt: on
+
+
+class Error(Exception):
+ """Base validation exception."""
+
+
+class SchemaError(Error):
+ """An error was encountered in the schema."""
+
+
+class Invalid(Error):
+ """The data was invalid.
+
+ :attr msg: The error message.
+ :attr path: The path to the error, as a list of keys in the source data.
+ :attr error_message: The actual error message that was raised, as a
+ string.
+
+ """
+
+ def __init__(
+ self,
+ message: str,
+ path: typing.Optional[typing.List[typing.Hashable]] = None,
+ error_message: typing.Optional[str] = None,
+ error_type: typing.Optional[str] = None,
+ ) -> None:
+ Error.__init__(self, message)
+ self._path = path or []
+ self._error_message = error_message or message
+ self.error_type = error_type
+
+ @property
+ def msg(self) -> str:
+ return self.args[0]
+
+ @property
+ def path(self) -> typing.List[typing.Hashable]:
+ return self._path
+
+ @property
+ def error_message(self) -> str:
+ return self._error_message
+
+ def __str__(self) -> str:
+ path = ' @ data[%s]' % ']['.join(map(repr, self.path)) if self.path else ''
+ output = Exception.__str__(self)
+ if self.error_type:
+ output += ' for ' + self.error_type
+ return output + path
+
+ def prepend(self, path: typing.List[typing.Hashable]) -> None:
+ self._path = path + self.path
+
+
+class MultipleInvalid(Invalid):
+ def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None:
+ self.errors = errors[:] if errors else []
+
+ def __repr__(self) -> str:
+ return 'MultipleInvalid(%r)' % self.errors
+
+ @property
+ def msg(self) -> str:
+ return self.errors[0].msg
+
+ @property
+ def path(self) -> typing.List[typing.Hashable]:
+ return self.errors[0].path
+
+ @property
+ def error_message(self) -> str:
+ return self.errors[0].error_message
+
+ def add(self, error: Invalid) -> None:
+ self.errors.append(error)
+
+ def __str__(self) -> str:
+ return str(self.errors[0])
+
+ def prepend(self, path: typing.List[typing.Hashable]) -> None:
+ for error in self.errors:
+ error.prepend(path)
+
+
+class RequiredFieldInvalid(Invalid):
+ """Required field was missing."""
+
+
+class ObjectInvalid(Invalid):
+ """The value we found was not an object."""
+
+
+class DictInvalid(Invalid):
+ """The value found was not a dict."""
+
+
+class ExclusiveInvalid(Invalid):
+ """More than one value found in exclusion group."""
+
+
+class InclusiveInvalid(Invalid):
+ """Not all values found in inclusion group."""
+
+
+class SequenceTypeInvalid(Invalid):
+ """The type found is not a sequence type."""
+
+
+class TypeInvalid(Invalid):
+ """The value was not of required type."""
+
+
+class ValueInvalid(Invalid):
+ """The value was found invalid by evaluation function."""
+
+
+class ContainsInvalid(Invalid):
+ """List does not contain item"""
+
+
+class ScalarInvalid(Invalid):
+ """Scalars did not match."""
+
+
+class CoerceInvalid(Invalid):
+ """Impossible to coerce value to type."""
+
+
+class AnyInvalid(Invalid):
+ """The value did not pass any validator."""
+
+
+class AllInvalid(Invalid):
+ """The value did not pass all validators."""
+
+
+class MatchInvalid(Invalid):
+ """The value does not match the given regular expression."""
+
+
+class RangeInvalid(Invalid):
+ """The value is not in given range."""
+
+
+class TrueInvalid(Invalid):
+ """The value is not True."""
+
+
+class FalseInvalid(Invalid):
+ """The value is not False."""
+
+
+class BooleanInvalid(Invalid):
+ """The value is not a boolean."""
+
+
+class UrlInvalid(Invalid):
+ """The value is not a URL."""
+
+
+class EmailInvalid(Invalid):
+ """The value is not an email address."""
+
+
+class FileInvalid(Invalid):
+ """The value is not a file."""
+
+
+class DirInvalid(Invalid):
+ """The value is not a directory."""
+
+
+class PathInvalid(Invalid):
+ """The value is not a path."""
+
+
+class LiteralInvalid(Invalid):
+ """The literal values do not match."""
+
+
+class LengthInvalid(Invalid):
+ pass
+
+
+class DatetimeInvalid(Invalid):
+ """The value is not a formatted datetime string."""
+
+
+class DateInvalid(Invalid):
+ """The value is not a formatted date string."""
+
+
+class InInvalid(Invalid):
+ pass
+
+
+class NotInInvalid(Invalid):
+ pass
+
+
+class ExactSequenceInvalid(Invalid):
+ pass
+
+
+class NotEnoughValid(Invalid):
+ """The value did not pass enough validations."""
+
+ pass
+
+
+class TooManyValid(Invalid):
+ """The value passed more than expected validations."""
+
+ pass
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/humanize.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/humanize.py
new file mode 100644
index 0000000000000..2fee63c6bdd9f
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/humanize.py
@@ -0,0 +1,57 @@
+# fmt: off
+import typing
+
+from voluptuous import Invalid, MultipleInvalid
+from voluptuous.error import Error
+from voluptuous.schema_builder import Schema
+
+# fmt: on
+
+MAX_VALIDATION_ERROR_ITEM_LENGTH = 500
+
+
+def _nested_getitem(
+ data: typing.Any, path: typing.List[typing.Hashable]
+) -> typing.Optional[typing.Any]:
+ for item_index in path:
+ try:
+ data = data[item_index]
+ except (KeyError, IndexError, TypeError):
+ # The index is not present in the dictionary, list or other
+ # indexable or data is not subscriptable
+ return None
+ return data
+
+
+def humanize_error(
+ data,
+ validation_error: Invalid,
+ max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH,
+) -> str:
+ """Provide a more helpful + complete validation error message than that provided automatically
+ Invalid and MultipleInvalid do not include the offending value in error messages,
+ and MultipleInvalid.__str__ only provides the first error.
+ """
+ if isinstance(validation_error, MultipleInvalid):
+ return '\n'.join(
+ sorted(
+ humanize_error(data, sub_error, max_sub_error_length)
+ for sub_error in validation_error.errors
+ )
+ )
+ else:
+ offending_item_summary = repr(_nested_getitem(data, validation_error.path))
+ if len(offending_item_summary) > max_sub_error_length:
+ offending_item_summary = (
+ offending_item_summary[: max_sub_error_length - 3] + '...'
+ )
+ return '%s. Got %s' % (validation_error, offending_item_summary)
+
+
+def validate_with_humanized_errors(
+ data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH
+) -> typing.Any:
+ try:
+ return schema(data)
+ except (Invalid, MultipleInvalid) as e:
+ raise Error(humanize_error(data, e, max_sub_error_length))
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/schema_builder.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/schema_builder.py
new file mode 100644
index 0000000000000..79001bfe3ec20
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/schema_builder.py
@@ -0,0 +1,1321 @@
+# fmt: off
+from __future__ import annotations
+
+import collections
+import inspect
+import itertools
+import re
+import sys
+import typing
+from collections.abc import Generator
+from contextlib import contextmanager
+from functools import cache, wraps
+
+from voluptuous import error as er
+from voluptuous.error import Error
+
+# fmt: on
+
+# options for extra keys
+PREVENT_EXTRA = 0 # any extra key not in schema will raise an error
+ALLOW_EXTRA = 1 # extra keys not in schema will be included in output
+REMOVE_EXTRA = 2 # extra keys not in schema will be excluded from output
+
+
+def _isnamedtuple(obj):
+ return isinstance(obj, tuple) and hasattr(obj, '_fields')
+
+
+class Undefined(object):
+ def __nonzero__(self):
+ return False
+
+ def __repr__(self):
+ return '...'
+
+
+UNDEFINED = Undefined()
+
+
+def Self() -> None:
+ raise er.SchemaError('"Self" should never be called')
+
+
+DefaultFactory = typing.Union[Undefined, typing.Callable[[], typing.Any]]
+
+
+def default_factory(value) -> DefaultFactory:
+ if value is UNDEFINED or callable(value):
+ return value
+ return lambda: value
+
+
+@contextmanager
+def raises(
+ exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None
+) -> Generator[None, None, None]:
+ try:
+ yield
+ except exc as e:
+ if msg is not None:
+ assert str(e) == msg, '%r != %r' % (str(e), msg)
+ if regex is not None:
+ assert re.search(regex, str(e)), '%r does not match %r' % (str(e), regex)
+ else:
+ raise AssertionError(f"Did not raise exception {exc.__name__}")
+
+
+def Extra(_) -> None:
+ """Allow keys in the data that are not present in the schema."""
+ raise er.SchemaError('"Extra" should never be called')
+
+
+# As extra() is never called there's no way to catch references to the
+# deprecated object, so we just leave an alias here instead.
+extra = Extra
+
+primitive_types = (bool, bytes, int, str, float, complex)
+
+# fmt: off
+Schemable = typing.Union[
+ 'Schema', 'Object',
+ collections.abc.Mapping,
+ list, tuple, frozenset, set,
+ bool, bytes, int, str, float, complex,
+ type, object, dict, None, typing.Callable
+]
+# fmt: on
+
+
+class Schema(object):
+ """A validation schema.
+
+ The schema is a Python tree-like structure where nodes are pattern
+ matched against corresponding trees of values.
+
+ Nodes can be values, in which case a direct comparison is used, types,
+ in which case an isinstance() check is performed, or callables, which will
+ validate and optionally convert the value.
+
+ We can equate schemas also.
+
+ For Example:
+
+ >>> v = Schema({Required('a'): str})
+ >>> v1 = Schema({Required('a'): str})
+ >>> v2 = Schema({Required('b'): str})
+ >>> assert v == v1
+ >>> assert v != v2
+
+ """
+
+ _extra_to_name = {
+ REMOVE_EXTRA: 'REMOVE_EXTRA',
+ ALLOW_EXTRA: 'ALLOW_EXTRA',
+ PREVENT_EXTRA: 'PREVENT_EXTRA',
+ }
+
+ def __init__(
+ self, schema: Schemable, required: bool = False, extra: int = PREVENT_EXTRA
+ ) -> None:
+ """Create a new Schema.
+
+ :param schema: Validation schema. See :module:`voluptuous` for details.
+ :param required: Keys defined in the schema must be in the data.
+ :param extra: Specify how extra keys in the data are treated:
+ - :const:`~voluptuous.PREVENT_EXTRA`: to disallow any undefined
+ extra keys (raise ``Invalid``).
+ - :const:`~voluptuous.ALLOW_EXTRA`: to include undefined extra
+ keys in the output.
+ - :const:`~voluptuous.REMOVE_EXTRA`: to exclude undefined extra keys
+ from the output.
+ - Any value other than the above defaults to
+ :const:`~voluptuous.PREVENT_EXTRA`
+ """
+ self.schema: typing.Any = schema
+ self.required = required
+ self.extra = int(extra) # ensure the value is an integer
+ self._compiled = self._compile(schema)
+
+ @classmethod
+ def infer(cls, data, **kwargs) -> Schema:
+ """Create a Schema from concrete data (e.g. an API response).
+
+ For example, this will take a dict like:
+
+ {
+ 'foo': 1,
+ 'bar': {
+ 'a': True,
+ 'b': False
+ },
+ 'baz': ['purple', 'monkey', 'dishwasher']
+ }
+
+ And return a Schema:
+
+ {
+ 'foo': int,
+ 'bar': {
+ 'a': bool,
+ 'b': bool
+ },
+ 'baz': [str]
+ }
+
+ Note: only very basic inference is supported.
+ """
+
+ def value_to_schema_type(value):
+ if isinstance(value, dict):
+ if len(value) == 0:
+ return dict
+ return {k: value_to_schema_type(v) for k, v in value.items()}
+ if isinstance(value, list):
+ if len(value) == 0:
+ return list
+ else:
+ return [value_to_schema_type(v) for v in value]
+ return type(value)
+
+ return cls(value_to_schema_type(data), **kwargs)
+
+ def __eq__(self, other):
+ if not isinstance(other, Schema):
+ return False
+ return other.schema == self.schema
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __str__(self):
+ return str(self.schema)
+
+ def __repr__(self):
+ return "" % (
+ self.schema,
+ self._extra_to_name.get(self.extra, '??'),
+ self.required,
+ id(self),
+ )
+
+ def __call__(self, data):
+ """Validate data against this schema."""
+ try:
+ return self._compiled([], data)
+ except er.MultipleInvalid:
+ raise
+ except er.Invalid as e:
+ raise er.MultipleInvalid([e])
+ # return self.validate([], self.schema, data)
+
+ def _compile(self, schema):
+ if schema is Extra:
+ return lambda _, v: v
+ if schema is Self:
+ return lambda p, v: self._compiled(p, v)
+ elif hasattr(schema, "__voluptuous_compile__"):
+ return schema.__voluptuous_compile__(self)
+ if isinstance(schema, Object):
+ return self._compile_object(schema)
+ if isinstance(schema, collections.abc.Mapping):
+ return self._compile_dict(schema)
+ elif isinstance(schema, list):
+ return self._compile_list(schema)
+ elif isinstance(schema, tuple):
+ return self._compile_tuple(schema)
+ elif isinstance(schema, (frozenset, set)):
+ return self._compile_set(schema)
+ type_ = type(schema)
+ if inspect.isclass(schema):
+ type_ = schema
+ if type_ in (*primitive_types, object, type(None)) or callable(schema):
+ return _compile_scalar(schema)
+ raise er.SchemaError('unsupported schema data type %r' % type(schema).__name__)
+
+ def _compile_mapping(self, schema, invalid_msg=None):
+ """Create validator for given mapping."""
+ invalid_msg = invalid_msg or 'mapping value'
+
+ # Keys that may be required
+ all_required_keys = set(
+ key
+ for key in schema
+ if key is not Extra
+ and (
+ (self.required and not isinstance(key, (Optional, Remove)))
+ or isinstance(key, Required)
+ )
+ )
+
+ # Keys that may have defaults
+ all_default_keys = set(
+ key
+ for key in schema
+ if isinstance(key, Required) or isinstance(key, Optional)
+ )
+
+ _compiled_schema = {}
+ for skey, svalue in schema.items():
+ new_key = self._compile(skey)
+ new_value = self._compile(svalue)
+ _compiled_schema[skey] = (new_key, new_value)
+
+ candidates = list(_iterate_mapping_candidates(_compiled_schema))
+
+ # After we have the list of candidates in the correct order, we want to apply some optimization so that each
+ # key in the data being validated will be matched against the relevant schema keys only.
+ # No point in matching against different keys
+ additional_candidates = []
+ candidates_by_key = {}
+ for skey, (ckey, cvalue) in candidates:
+ if type(skey) in primitive_types:
+ candidates_by_key.setdefault(skey, []).append((skey, (ckey, cvalue)))
+ elif isinstance(skey, Marker) and type(skey.schema) in primitive_types:
+ candidates_by_key.setdefault(skey.schema, []).append(
+ (skey, (ckey, cvalue))
+ )
+ else:
+ # These are wildcards such as 'int', 'str', 'Remove' and others which should be applied to all keys
+ additional_candidates.append((skey, (ckey, cvalue)))
+
+ def validate_mapping(path, iterable, out):
+ required_keys = all_required_keys.copy()
+
+ # Build a map of all provided key-value pairs.
+ # The type(out) is used to retain ordering in case a ordered
+ # map type is provided as input.
+ key_value_map = type(out)()
+ for key, value in iterable:
+ key_value_map[key] = value
+
+ # Insert default values for non-existing keys.
+ for key in all_default_keys:
+ if (
+ not isinstance(key.default, Undefined)
+ and key.schema not in key_value_map
+ ):
+ # A default value has been specified for this missing
+ # key, insert it.
+ key_value_map[key.schema] = key.default()
+
+ errors = []
+ for key, value in key_value_map.items():
+ key_path = path + [key]
+ remove_key = False
+
+ # Optimization. Validate against the matching key first, then fallback to the rest
+ relevant_candidates = itertools.chain(
+ candidates_by_key.get(key, []), additional_candidates
+ )
+
+ # compare each given key/value against all compiled key/values
+ # schema key, (compiled key, compiled value)
+ error = None
+ for skey, (ckey, cvalue) in relevant_candidates:
+ try:
+ new_key = ckey(key_path, key)
+ except er.Invalid as e:
+ if len(e.path) > len(key_path):
+ raise
+ if not error or len(e.path) > len(error.path):
+ error = e
+ continue
+ # Backtracking is not performed once a key is selected, so if
+ # the value is invalid we immediately throw an exception.
+ exception_errors = []
+ # check if the key is marked for removal
+ is_remove = new_key is Remove
+ try:
+ cval = cvalue(key_path, value)
+ # include if it's not marked for removal
+ if not is_remove:
+ out[new_key] = cval
+ else:
+ remove_key = True
+ continue
+ except er.MultipleInvalid as e:
+ exception_errors.extend(e.errors)
+ except er.Invalid as e:
+ exception_errors.append(e)
+
+ if exception_errors:
+ if is_remove or remove_key:
+ continue
+ for err in exception_errors:
+ if len(err.path) <= len(key_path):
+ err.error_type = invalid_msg
+ errors.append(err)
+ # If there is a validation error for a required
+ # key, this means that the key was provided.
+ # Discard the required key so it does not
+ # create an additional, noisy exception.
+ required_keys.discard(skey)
+ break
+
+ # Key and value okay, mark as found in case it was
+ # a Required() field.
+ required_keys.discard(skey)
+
+ break
+ else:
+ if remove_key:
+ # remove key
+ continue
+ elif self.extra == ALLOW_EXTRA:
+ out[key] = value
+ elif self.extra == REMOVE_EXTRA:
+ # ignore the key so it's removed from output
+ continue
+ elif error:
+ errors.append(error)
+ else:
+ errors.append(er.Invalid('extra keys not allowed', key_path))
+
+ # for any required keys left that weren't found and don't have defaults:
+ for key in required_keys:
+ msg = (
+ key.msg
+ if hasattr(key, 'msg') and key.msg
+ else 'required key not provided'
+ )
+ errors.append(er.RequiredFieldInvalid(msg, path + [key]))
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ return out
+
+ return validate_mapping
+
+ def _compile_object(self, schema):
+ """Validate an object.
+
+ Has the same behavior as dictionary validator but work with object
+ attributes.
+
+ For example:
+
+ >>> class Structure(object):
+ ... def __init__(self, one=None, three=None):
+ ... self.one = one
+ ... self.three = three
+ ...
+ >>> validate = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure))
+ >>> with raises(er.MultipleInvalid, "not a valid value for object value @ data['one']"):
+ ... validate(Structure(one='three'))
+
+ """
+ base_validate = self._compile_mapping(schema, invalid_msg='object value')
+
+ def validate_object(path, data):
+ if schema.cls is not UNDEFINED and not isinstance(data, schema.cls):
+ raise er.ObjectInvalid('expected a {0!r}'.format(schema.cls), path)
+ iterable = _iterate_object(data)
+ iterable = filter(lambda item: item[1] is not None, iterable)
+ out = base_validate(path, iterable, {})
+ return type(data)(**out)
+
+ return validate_object
+
+ def _compile_dict(self, schema):
+ """Validate a dictionary.
+
+ A dictionary schema can contain a set of values, or at most one
+ validator function/type.
+
+ A dictionary schema will only validate a dictionary:
+
+ >>> validate = Schema({})
+ >>> with raises(er.MultipleInvalid, 'expected a dictionary'):
+ ... validate([])
+
+ An invalid dictionary value:
+
+ >>> validate = Schema({'one': 'two', 'three': 'four'})
+ >>> with raises(er.MultipleInvalid, "not a valid value for dictionary value @ data['one']"):
+ ... validate({'one': 'three'})
+
+ An invalid key:
+
+ >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['two']"):
+ ... validate({'two': 'three'})
+
+
+ Validation function, in this case the "int" type:
+
+ >>> validate = Schema({'one': 'two', 'three': 'four', int: str})
+
+ Valid integer input:
+
+ >>> validate({10: 'twenty'})
+ {10: 'twenty'}
+
+ By default, a "type" in the schema (in this case "int") will be used
+ purely to validate that the corresponding value is of that type. It
+ will not Coerce the value:
+
+ >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['10']"):
+ ... validate({'10': 'twenty'})
+
+ Wrap them in the Coerce() function to achieve this:
+ >>> from voluptuous import Coerce
+ >>> validate = Schema({'one': 'two', 'three': 'four',
+ ... Coerce(int): str})
+ >>> validate({'10': 'twenty'})
+ {10: 'twenty'}
+
+ Custom message for required key
+
+ >>> validate = Schema({Required('one', 'required'): 'two'})
+ >>> with raises(er.MultipleInvalid, "required @ data['one']"):
+ ... validate({})
+
+ (This is to avoid unexpected surprises.)
+
+ Multiple errors for nested field in a dict:
+
+ >>> validate = Schema({
+ ... 'adict': {
+ ... 'strfield': str,
+ ... 'intfield': int
+ ... }
+ ... })
+ >>> try:
+ ... validate({
+ ... 'adict': {
+ ... 'strfield': 123,
+ ... 'intfield': 'one'
+ ... }
+ ... })
+ ... except er.MultipleInvalid as e:
+ ... print(sorted(str(i) for i in e.errors)) # doctest: +NORMALIZE_WHITESPACE
+ ["expected int for dictionary value @ data['adict']['intfield']",
+ "expected str for dictionary value @ data['adict']['strfield']"]
+
+ """
+ base_validate = self._compile_mapping(schema, invalid_msg='dictionary value')
+
+ groups_of_exclusion = {}
+ groups_of_inclusion = {}
+ for node in schema:
+ if isinstance(node, Exclusive):
+ g = groups_of_exclusion.setdefault(node.group_of_exclusion, [])
+ g.append(node)
+ elif isinstance(node, Inclusive):
+ g = groups_of_inclusion.setdefault(node.group_of_inclusion, [])
+ g.append(node)
+
+ def validate_dict(path, data):
+ if not isinstance(data, dict):
+ raise er.DictInvalid('expected a dictionary', path)
+
+ errors = []
+ for label, group in groups_of_exclusion.items():
+ exists = False
+ for exclusive in group:
+ if exclusive.schema in data:
+ if exists:
+ msg = (
+ exclusive.msg
+ if hasattr(exclusive, 'msg') and exclusive.msg
+ else "two or more values in the same group of exclusion '%s'"
+ % label
+ )
+ next_path = path + [VirtualPathComponent(label)]
+ errors.append(er.ExclusiveInvalid(msg, next_path))
+ break
+ exists = True
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ for label, group in groups_of_inclusion.items():
+ included = [node.schema in data for node in group]
+ if any(included) and not all(included):
+ msg = (
+ "some but not all values in the same group of inclusion '%s'"
+ % label
+ )
+ for g in group:
+ if hasattr(g, 'msg') and g.msg:
+ msg = g.msg
+ break
+ next_path = path + [VirtualPathComponent(label)]
+ errors.append(er.InclusiveInvalid(msg, next_path))
+ break
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ out = data.__class__()
+ return base_validate(path, data.items(), out)
+
+ return validate_dict
+
+ def _compile_sequence(self, schema, seq_type):
+ """Validate a sequence type.
+
+ This is a sequence of valid values or validators tried in order.
+
+ >>> validator = Schema(['one', 'two', int])
+ >>> validator(['one'])
+ ['one']
+ >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'):
+ ... validator([3.5])
+ >>> validator([1])
+ [1]
+ """
+ _compiled = [self._compile(s) for s in schema]
+ seq_type_name = seq_type.__name__
+
+ def validate_sequence(path, data):
+ if not isinstance(data, seq_type):
+ raise er.SequenceTypeInvalid('expected a %s' % seq_type_name, path)
+
+ # Empty seq schema, reject any data.
+ if not schema:
+ if data:
+ raise er.MultipleInvalid(
+ [er.ValueInvalid('not a valid value', path if path else data)]
+ )
+ return data
+
+ out = []
+ invalid = None
+ errors = []
+ index_path = UNDEFINED
+ for i, value in enumerate(data):
+ index_path = path + [i]
+ invalid = None
+ for validate in _compiled:
+ try:
+ cval = validate(index_path, value)
+ if cval is not Remove: # do not include Remove values
+ out.append(cval)
+ break
+ except er.Invalid as e:
+ if len(e.path) > len(index_path):
+ raise
+ invalid = e
+ else:
+ errors.append(invalid)
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ if _isnamedtuple(data):
+ return type(data)(*out)
+ else:
+ return type(data)(out)
+
+ return validate_sequence
+
+ def _compile_tuple(self, schema):
+ """Validate a tuple.
+
+ A tuple is a sequence of valid values or validators tried in order.
+
+ >>> validator = Schema(('one', 'two', int))
+ >>> validator(('one',))
+ ('one',)
+ >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'):
+ ... validator((3.5,))
+ >>> validator((1,))
+ (1,)
+ """
+ return self._compile_sequence(schema, tuple)
+
+ def _compile_list(self, schema):
+ """Validate a list.
+
+ A list is a sequence of valid values or validators tried in order.
+
+ >>> validator = Schema(['one', 'two', int])
+ >>> validator(['one'])
+ ['one']
+ >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'):
+ ... validator([3.5])
+ >>> validator([1])
+ [1]
+ """
+ return self._compile_sequence(schema, list)
+
+ def _compile_set(self, schema):
+ """Validate a set.
+
+ A set is an unordered collection of unique elements.
+
+ >>> validator = Schema({int})
+ >>> validator(set([42])) == set([42])
+ True
+ >>> with raises(er.Invalid, 'expected a set'):
+ ... validator(42)
+ >>> with raises(er.MultipleInvalid, 'invalid value in set'):
+ ... validator(set(['a']))
+ """
+ type_ = type(schema)
+ type_name = type_.__name__
+
+ def validate_set(path, data):
+ if not isinstance(data, type_):
+ raise er.Invalid('expected a %s' % type_name, path)
+
+ _compiled = [self._compile(s) for s in schema]
+ errors = []
+ for value in data:
+ for validate in _compiled:
+ try:
+ validate(path, value)
+ break
+ except er.Invalid:
+ pass
+ else:
+ invalid = er.Invalid('invalid value in %s' % type_name, path)
+ errors.append(invalid)
+
+ if errors:
+ raise er.MultipleInvalid(errors)
+
+ return data
+
+ return validate_set
+
+ def extend(
+ self,
+ schema: Schemable,
+ required: typing.Optional[bool] = None,
+ extra: typing.Optional[int] = None,
+ ) -> Schema:
+ """Create a new `Schema` by merging this and the provided `schema`.
+
+ Neither this `Schema` nor the provided `schema` are modified. The
+ resulting `Schema` inherits the `required` and `extra` parameters of
+ this, unless overridden.
+
+ Both schemas must be dictionary-based.
+
+ :param schema: dictionary to extend this `Schema` with
+ :param required: if set, overrides `required` of this `Schema`
+ :param extra: if set, overrides `extra` of this `Schema`
+ """
+
+ assert isinstance(self.schema, dict) and isinstance(
+ schema, dict
+ ), 'Both schemas must be dictionary-based'
+
+ result = self.schema.copy()
+
+ # returns the key that may have been passed as an argument to Marker constructor
+ def key_literal(key):
+ return key.schema if isinstance(key, Marker) else key
+
+ # build a map that takes the key literals to the needed objects
+ # literal -> Required|Optional|literal
+ result_key_map = dict((key_literal(key), key) for key in result)
+
+ # for each item in the extension schema, replace duplicates
+ # or add new keys
+ for key, value in schema.items():
+ # if the key is already in the dictionary, we need to replace it
+ # transform key to literal before checking presence
+ if key_literal(key) in result_key_map:
+ result_key = result_key_map[key_literal(key)]
+ result_value = result[result_key]
+
+ # if both are dictionaries, we need to extend recursively
+ # create the new extended sub schema, then remove the old key and add the new one
+ if isinstance(result_value, dict) and isinstance(value, dict):
+ new_value = Schema(result_value).extend(value).schema
+ del result[result_key]
+ result[key] = new_value
+ # one or the other or both are not sub-schemas, simple replacement is fine
+ # remove old key and add new one
+ else:
+ del result[result_key]
+ result[key] = value
+
+ # key is new and can simply be added
+ else:
+ result[key] = value
+
+ # recompile and send old object
+ result_cls = type(self)
+ result_required = required if required is not None else self.required
+ result_extra = extra if extra is not None else self.extra
+ return result_cls(result, required=result_required, extra=result_extra)
+
+
+def _compile_scalar(schema):
+ """A scalar value.
+
+ The schema can either be a value or a type.
+
+ >>> _compile_scalar(int)([], 1)
+ 1
+ >>> with raises(er.Invalid, 'expected float'):
+ ... _compile_scalar(float)([], '1')
+
+ Callables have
+ >>> _compile_scalar(lambda v: float(v))([], '1')
+ 1.0
+
+ As a convenience, ValueError's are trapped:
+
+ >>> with raises(er.Invalid, 'not a valid value'):
+ ... _compile_scalar(lambda v: float(v))([], 'a')
+ """
+ if inspect.isclass(schema):
+
+ def validate_instance(path, data):
+ if isinstance(data, schema):
+ return data
+ else:
+ msg = 'expected %s' % schema.__name__
+ raise er.TypeInvalid(msg, path)
+
+ return validate_instance
+
+ if callable(schema):
+
+ def validate_callable(path, data):
+ try:
+ return schema(data)
+ except ValueError:
+ raise er.ValueInvalid('not a valid value', path)
+ except er.Invalid as e:
+ e.prepend(path)
+ raise
+
+ return validate_callable
+
+ def validate_value(path, data):
+ if data != schema:
+ raise er.ScalarInvalid('not a valid value', path)
+ return data
+
+ return validate_value
+
+
+def _compile_itemsort():
+ '''return sort function of mappings'''
+
+ def is_extra(key_):
+ return key_ is Extra
+
+ def is_remove(key_):
+ return isinstance(key_, Remove)
+
+ def is_marker(key_):
+ return isinstance(key_, Marker)
+
+ def is_type(key_):
+ return inspect.isclass(key_)
+
+ def is_callable(key_):
+ return callable(key_)
+
+ # priority list for map sorting (in order of checking)
+ # We want Extra to match last, because it's a catch-all. On the other hand,
+ # Remove markers should match first (since invalid values will not
+ # raise an Error, instead the validator will check if other schemas match
+ # the same value).
+ priority = [
+ (1, is_remove), # Remove highest priority after values
+ (2, is_marker), # then other Markers
+ (4, is_type), # types/classes lowest before Extra
+ (3, is_callable), # callables after markers
+ (5, is_extra), # Extra lowest priority
+ ]
+
+ def item_priority(item_):
+ key_ = item_[0]
+ for i, check_ in priority:
+ if check_(key_):
+ return i
+ # values have highest priorities
+ return 0
+
+ return item_priority
+
+
+_sort_item = _compile_itemsort()
+
+
+def _iterate_mapping_candidates(schema):
+ """Iterate over schema in a meaningful order."""
+ # Without this, Extra might appear first in the iterator, and fail to
+ # validate a key even though it's a Required that has its own validation,
+ # generating a false positive.
+ return sorted(schema.items(), key=_sort_item)
+
+
+def _iterate_object(obj):
+ """Return iterator over object attributes. Respect objects with
+ defined __slots__.
+
+ """
+ d = {}
+ try:
+ d = vars(obj)
+ except TypeError:
+ # maybe we have named tuple here?
+ if hasattr(obj, '_asdict'):
+ d = obj._asdict()
+ for item in d.items():
+ yield item
+ try:
+ slots = obj.__slots__
+ except AttributeError:
+ pass
+ else:
+ for key in slots:
+ if key != '__dict__':
+ yield (key, getattr(obj, key))
+
+
+class Msg(object):
+ """Report a user-friendly message if a schema fails to validate.
+
+ >>> validate = Schema(
+ ... Msg(['one', 'two', int],
+ ... 'should be one of "one", "two" or an integer'))
+ >>> with raises(er.MultipleInvalid, 'should be one of "one", "two" or an integer'):
+ ... validate(['three'])
+
+ Messages are only applied to invalid direct descendants of the schema:
+
+ >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!'))
+ >>> with raises(er.MultipleInvalid, 'expected int @ data[0][0]'):
+ ... validate([['three']])
+
+ The type which is thrown can be overridden but needs to be a subclass of Invalid
+
+ >>> with raises(er.SchemaError, 'Msg can only use subclases of Invalid as custom class'):
+ ... validate = Schema(Msg([int], 'should be int', cls=KeyError))
+
+ If you do use a subclass of Invalid, that error will be thrown (wrapped in a MultipleInvalid)
+
+ >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!', cls=er.RangeInvalid))
+ >>> try:
+ ... validate(['three'])
+ ... except er.MultipleInvalid as e:
+ ... assert isinstance(e.errors[0], er.RangeInvalid)
+ """
+
+ def __init__(
+ self,
+ schema: Schemable,
+ msg: str,
+ cls: typing.Optional[typing.Type[Error]] = None,
+ ) -> None:
+ if cls and not issubclass(cls, er.Invalid):
+ raise er.SchemaError(
+ "Msg can only use subclases of Invalid as custom class"
+ )
+ self._schema = schema
+ self.schema = Schema(schema)
+ self.msg = msg
+ self.cls = cls
+
+ def __call__(self, v):
+ try:
+ return self.schema(v)
+ except er.Invalid as e:
+ if len(e.path) > 1:
+ raise e
+ else:
+ raise (self.cls or er.Invalid)(self.msg)
+
+ def __repr__(self):
+ return 'Msg(%s, %s, cls=%s)' % (self._schema, self.msg, self.cls)
+
+
+class Object(dict):
+ """Indicate that we should work with attributes, not keys."""
+
+ def __init__(self, schema: typing.Any, cls: object = UNDEFINED) -> None:
+ self.cls = cls
+ super(Object, self).__init__(schema)
+
+
+class VirtualPathComponent(str):
+ def __str__(self):
+ return '<' + self + '>'
+
+ def __repr__(self):
+ return self.__str__()
+
+
+class Marker(object):
+ """Mark nodes for special treatment.
+
+ `description` is an optional field, unused by Voluptuous itself, but can be
+ introspected by any external tool, for example to generate schema documentation.
+ """
+
+ __slots__ = ('schema', '_schema', 'msg', 'description', '_hash')
+
+ def __init__(
+ self,
+ schema_: Schemable,
+ msg: typing.Optional[str] = None,
+ description: typing.Any | None = None,
+ ) -> None:
+ self.schema: typing.Any = schema_
+ self._schema = Schema(schema_)
+ self.msg = msg
+ self.description = description
+
+ def __call__(self, v):
+ try:
+ return self._schema(v)
+ except er.Invalid as e:
+ if not self.msg or len(e.path) > 1:
+ raise
+ raise er.Invalid(self.msg)
+
+ def __str__(self):
+ return str(self.schema)
+
+ def __repr__(self):
+ return repr(self.schema)
+
+ def __lt__(self, other):
+ if isinstance(other, Marker):
+ return self.schema < other.schema
+ return self.schema < other
+
+ def __eq__(self, other):
+ return self.schema == other
+
+ def __ne__(self, other):
+ return not (self.schema == other)
+
+ def __hash__(self):
+ if not hasattr(self, '_hash'):
+ self._hash = hash(self.schema)
+ return self._hash
+
+
+class Optional(Marker):
+ """Mark a node in the schema as optional, and optionally provide a default
+
+ >>> schema = Schema({Optional('key'): str})
+ >>> schema({})
+ {}
+ >>> schema = Schema({Optional('key', default='value'): str})
+ >>> schema({})
+ {'key': 'value'}
+ >>> schema = Schema({Optional('key', default=list): list})
+ >>> schema({})
+ {'key': []}
+
+ If 'required' flag is set for an entire schema, optional keys aren't required
+
+ >>> schema = Schema({
+ ... Optional('key'): str,
+ ... 'key2': str
+ ... }, required=True)
+ >>> schema({'key2':'value'})
+ {'key2': 'value'}
+ """
+
+ def __init__(
+ self,
+ schema: Schemable,
+ msg: typing.Optional[str] = None,
+ default: typing.Any = UNDEFINED,
+ description: typing.Any | None = None,
+ ) -> None:
+ super(Optional, self).__init__(schema, msg=msg, description=description)
+ self.default = default_factory(default)
+
+
+class Exclusive(Optional):
+ """Mark a node in the schema as exclusive.
+
+ Exclusive keys inherited from Optional:
+
+ >>> schema = Schema({Exclusive('alpha', 'angles'): int, Exclusive('beta', 'angles'): int})
+ >>> schema({'alpha': 30})
+ {'alpha': 30}
+
+ Keys inside a same group of exclusion cannot be together, it only makes sense for dictionaries:
+
+ >>> with raises(er.MultipleInvalid, "two or more values in the same group of exclusion 'angles' @ data[]"):
+ ... schema({'alpha': 30, 'beta': 45})
+
+ For example, API can provides multiple types of authentication, but only one works in the same time:
+
+ >>> msg = 'Please, use only one type of authentication at the same time.'
+ >>> schema = Schema({
+ ... Exclusive('classic', 'auth', msg=msg):{
+ ... Required('email'): str,
+ ... Required('password'): str
+ ... },
+ ... Exclusive('internal', 'auth', msg=msg):{
+ ... Required('secret_key'): str
+ ... },
+ ... Exclusive('social', 'auth', msg=msg):{
+ ... Required('social_network'): str,
+ ... Required('token'): str
+ ... }
+ ... })
+
+ >>> with raises(er.MultipleInvalid, "Please, use only one type of authentication at the same time. @ data[]"):
+ ... schema({'classic': {'email': 'foo@example.com', 'password': 'bar'},
+ ... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}})
+ """
+
+ def __init__(
+ self,
+ schema: Schemable,
+ group_of_exclusion: str,
+ msg: typing.Optional[str] = None,
+ description: typing.Any | None = None,
+ ) -> None:
+ super(Exclusive, self).__init__(schema, msg=msg, description=description)
+ self.group_of_exclusion = group_of_exclusion
+
+
+class Inclusive(Optional):
+ """Mark a node in the schema as inclusive.
+
+ Inclusive keys inherited from Optional:
+
+ >>> schema = Schema({
+ ... Inclusive('filename', 'file'): str,
+ ... Inclusive('mimetype', 'file'): str
+ ... })
+ >>> data = {'filename': 'dog.jpg', 'mimetype': 'image/jpeg'}
+ >>> data == schema(data)
+ True
+
+ Keys inside a same group of inclusive must exist together, it only makes sense for dictionaries:
+
+ >>> with raises(er.MultipleInvalid, "some but not all values in the same group of inclusion 'file' @ data[]"):
+ ... schema({'filename': 'dog.jpg'})
+
+ If none of the keys in the group are present, it is accepted:
+
+ >>> schema({})
+ {}
+
+ For example, API can return 'height' and 'width' together, but not separately.
+
+ >>> msg = "Height and width must exist together"
+ >>> schema = Schema({
+ ... Inclusive('height', 'size', msg=msg): int,
+ ... Inclusive('width', 'size', msg=msg): int
+ ... })
+
+ >>> with raises(er.MultipleInvalid, msg + " @ data[]"):
+ ... schema({'height': 100})
+
+ >>> with raises(er.MultipleInvalid, msg + " @ data[]"):
+ ... schema({'width': 100})
+
+ >>> data = {'height': 100, 'width': 100}
+ >>> data == schema(data)
+ True
+ """
+
+ def __init__(
+ self,
+ schema: Schemable,
+ group_of_inclusion: str,
+ msg: typing.Optional[str] = None,
+ description: typing.Any | None = None,
+ default: typing.Any = UNDEFINED,
+ ) -> None:
+ super(Inclusive, self).__init__(
+ schema, msg=msg, default=default, description=description
+ )
+ self.group_of_inclusion = group_of_inclusion
+
+
+class Required(Marker):
+ """Mark a node in the schema as being required, and optionally provide a default value.
+
+ >>> schema = Schema({Required('key'): str})
+ >>> with raises(er.MultipleInvalid, "required key not provided @ data['key']"):
+ ... schema({})
+
+ >>> schema = Schema({Required('key', default='value'): str})
+ >>> schema({})
+ {'key': 'value'}
+ >>> schema = Schema({Required('key', default=list): list})
+ >>> schema({})
+ {'key': []}
+ """
+
+ def __init__(
+ self,
+ schema: Schemable,
+ msg: typing.Optional[str] = None,
+ default: typing.Any = UNDEFINED,
+ description: typing.Any | None = None,
+ ) -> None:
+ super(Required, self).__init__(schema, msg=msg, description=description)
+ self.default = default_factory(default)
+
+
+class Remove(Marker):
+ """Mark a node in the schema to be removed and excluded from the validated
+ output. Keys that fail validation will not raise ``Invalid``. Instead, these
+ keys will be treated as extras.
+
+ >>> schema = Schema({str: int, Remove(int): str})
+ >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data[1]"):
+ ... schema({'keep': 1, 1: 1.0})
+ >>> schema({1: 'red', 'red': 1, 2: 'green'})
+ {'red': 1}
+ >>> schema = Schema([int, Remove(float), Extra])
+ >>> schema([1, 2, 3, 4.0, 5, 6.0, '7'])
+ [1, 2, 3, 5, '7']
+ """
+
+ def __init__(
+ self,
+ schema_: Schemable,
+ msg: typing.Optional[str] = None,
+ description: typing.Any | None = None,
+ ) -> None:
+ super().__init__(schema_, msg, description)
+ self.__hash__ = cache(lambda: object.__hash__(self)) # type: ignore[method-assign]
+
+ def __call__(self, schema: Schemable):
+ super(Remove, self).__call__(schema)
+ return self.__class__
+
+ def __repr__(self):
+ return "Remove(%r)" % (self.schema,)
+
+
+def message(
+ default: typing.Optional[str] = None,
+ cls: typing.Optional[typing.Type[Error]] = None,
+) -> typing.Callable:
+ """Convenience decorator to allow functions to provide a message.
+
+ Set a default message:
+
+ >>> @message('not an integer')
+ ... def isint(v):
+ ... return int(v)
+
+ >>> validate = Schema(isint())
+ >>> with raises(er.MultipleInvalid, 'not an integer'):
+ ... validate('a')
+
+ The message can be overridden on a per validator basis:
+
+ >>> validate = Schema(isint('bad'))
+ >>> with raises(er.MultipleInvalid, 'bad'):
+ ... validate('a')
+
+ The class thrown too:
+
+ >>> class IntegerInvalid(er.Invalid): pass
+ >>> validate = Schema(isint('bad', clsoverride=IntegerInvalid))
+ >>> try:
+ ... validate('a')
+ ... except er.MultipleInvalid as e:
+ ... assert isinstance(e.errors[0], IntegerInvalid)
+ """
+ if cls and not issubclass(cls, er.Invalid):
+ raise er.SchemaError(
+ "message can only use subclases of Invalid as custom class"
+ )
+
+ def decorator(f):
+ @wraps(f)
+ def check(msg=None, clsoverride=None):
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except ValueError:
+ raise (clsoverride or cls or er.ValueInvalid)(
+ msg or default or 'invalid value'
+ )
+
+ return wrapper
+
+ return check
+
+ return decorator
+
+
+def _args_to_dict(func, args):
+ """Returns argument names as values as key-value pairs."""
+ if sys.version_info >= (3, 0):
+ arg_count = func.__code__.co_argcount
+ arg_names = func.__code__.co_varnames[:arg_count]
+ else:
+ arg_count = func.func_code.co_argcount
+ arg_names = func.func_code.co_varnames[:arg_count]
+
+ arg_value_list = list(args)
+ arguments = dict(
+ (arg_name, arg_value_list[i])
+ for i, arg_name in enumerate(arg_names)
+ if i < len(arg_value_list)
+ )
+ return arguments
+
+
+def _merge_args_with_kwargs(args_dict, kwargs_dict):
+ """Merge args with kwargs."""
+ ret = args_dict.copy()
+ ret.update(kwargs_dict)
+ return ret
+
+
+def validate(*a, **kw) -> typing.Callable:
+ """Decorator for validating arguments of a function against a given schema.
+
+ Set restrictions for arguments:
+
+ >>> @validate(arg1=int, arg2=int)
+ ... def foo(arg1, arg2):
+ ... return arg1 * arg2
+
+ Set restriction for returned value:
+
+ >>> @validate(arg=int, __return__=int)
+ ... def bar(arg1):
+ ... return arg1 * 2
+
+ """
+ RETURNS_KEY = '__return__'
+
+ def validate_schema_decorator(func):
+ returns_defined = False
+ returns = None
+
+ schema_args_dict = _args_to_dict(func, a)
+ schema_arguments = _merge_args_with_kwargs(schema_args_dict, kw)
+
+ if RETURNS_KEY in schema_arguments:
+ returns_defined = True
+ returns = schema_arguments[RETURNS_KEY]
+ del schema_arguments[RETURNS_KEY]
+
+ input_schema = (
+ Schema(schema_arguments, extra=ALLOW_EXTRA)
+ if len(schema_arguments) != 0
+ else lambda x: x
+ )
+ output_schema = Schema(returns) if returns_defined else lambda x: x
+
+ @wraps(func)
+ def func_wrapper(*args, **kwargs):
+ args_dict = _args_to_dict(func, args)
+ arguments = _merge_args_with_kwargs(args_dict, kwargs)
+ validated_arguments = input_schema(arguments)
+ output = func(**validated_arguments)
+ return output_schema(output)
+
+ return func_wrapper
+
+ return validate_schema_decorator
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/util.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/util.py
new file mode 100644
index 0000000000000..5be35a0d5c74b
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/util.py
@@ -0,0 +1,149 @@
+# F401: "imported but unused"
+# fmt: off
+import typing
+
+from voluptuous import validators # noqa: F401
+from voluptuous.error import Invalid, LiteralInvalid, TypeInvalid # noqa: F401
+from voluptuous.schema_builder import DefaultFactory # noqa: F401
+from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401
+
+# fmt: on
+
+__author__ = 'tusharmakkar08'
+
+
+def Lower(v: str) -> str:
+ """Transform a string to lower case.
+
+ >>> s = Schema(Lower)
+ >>> s('HI')
+ 'hi'
+ """
+ return str(v).lower()
+
+
+def Upper(v: str) -> str:
+ """Transform a string to upper case.
+
+ >>> s = Schema(Upper)
+ >>> s('hi')
+ 'HI'
+ """
+ return str(v).upper()
+
+
+def Capitalize(v: str) -> str:
+ """Capitalise a string.
+
+ >>> s = Schema(Capitalize)
+ >>> s('hello world')
+ 'Hello world'
+ """
+ return str(v).capitalize()
+
+
+def Title(v: str) -> str:
+ """Title case a string.
+
+ >>> s = Schema(Title)
+ >>> s('hello world')
+ 'Hello World'
+ """
+ return str(v).title()
+
+
+def Strip(v: str) -> str:
+ """Strip whitespace from a string.
+
+ >>> s = Schema(Strip)
+ >>> s(' hello world ')
+ 'hello world'
+ """
+ return str(v).strip()
+
+
+class DefaultTo(object):
+ """Sets a value to default_value if none provided.
+
+ >>> s = Schema(DefaultTo(42))
+ >>> s(None)
+ 42
+ >>> s = Schema(DefaultTo(list))
+ >>> s(None)
+ []
+ """
+
+ def __init__(self, default_value, msg: typing.Optional[str] = None) -> None:
+ self.default_value = default_factory(default_value)
+ self.msg = msg
+
+ def __call__(self, v):
+ if v is None:
+ v = self.default_value()
+ return v
+
+ def __repr__(self):
+ return 'DefaultTo(%s)' % (self.default_value(),)
+
+
+class SetTo(object):
+ """Set a value, ignoring any previous value.
+
+ >>> s = Schema(validators.Any(int, SetTo(42)))
+ >>> s(2)
+ 2
+ >>> s("foo")
+ 42
+ """
+
+ def __init__(self, value) -> None:
+ self.value = default_factory(value)
+
+ def __call__(self, v):
+ return self.value()
+
+ def __repr__(self):
+ return 'SetTo(%s)' % (self.value(),)
+
+
+class Set(object):
+ """Convert a list into a set.
+
+ >>> s = Schema(Set())
+ >>> s([]) == set([])
+ True
+ >>> s([1, 2]) == set([1, 2])
+ True
+ >>> with raises(Invalid, regex="^cannot be presented as set: "):
+ ... s([set([1, 2]), set([3, 4])])
+ """
+
+ def __init__(self, msg: typing.Optional[str] = None) -> None:
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ set_v = set(v)
+ except Exception as e:
+ raise TypeInvalid(self.msg or 'cannot be presented as set: {0}'.format(e))
+ return set_v
+
+ def __repr__(self):
+ return 'Set()'
+
+
+class Literal(object):
+ def __init__(self, lit) -> None:
+ self.lit = lit
+
+ def __call__(self, value, msg: typing.Optional[str] = None):
+ if self.lit != value:
+ raise LiteralInvalid(msg or '%s not match for %s' % (value, self.lit))
+ else:
+ return self.lit
+
+ def __str__(self):
+ return str(self.lit)
+
+ def __repr__(self):
+ return repr(self.lit)
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/validators.py b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/validators.py
new file mode 100644
index 0000000000000..f207536aac247
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/python/voluptuous/validators.py
@@ -0,0 +1,1270 @@
+# fmt: off
+from __future__ import annotations
+
+import datetime
+import os
+import re
+import sys
+import typing
+from decimal import Decimal, InvalidOperation
+from functools import wraps
+
+from voluptuous.error import (
+ AllInvalid,
+ AnyInvalid,
+ BooleanInvalid,
+ CoerceInvalid,
+ ContainsInvalid,
+ DateInvalid,
+ DatetimeInvalid,
+ DirInvalid,
+ EmailInvalid,
+ ExactSequenceInvalid,
+ FalseInvalid,
+ FileInvalid,
+ InInvalid,
+ Invalid,
+ LengthInvalid,
+ MatchInvalid,
+ MultipleInvalid,
+ NotEnoughValid,
+ NotInInvalid,
+ PathInvalid,
+ RangeInvalid,
+ TooManyValid,
+ TrueInvalid,
+ TypeInvalid,
+ UrlInvalid,
+)
+
+# F401: flake8 complains about 'raises' not being used, but it is used in doctests
+from voluptuous.schema_builder import Schema, Schemable, message, raises # noqa: F401
+
+if typing.TYPE_CHECKING:
+ from _typeshed import SupportsAllComparisons
+
+# fmt: on
+
+
+Enum: typing.Union[type, None]
+try:
+ from enum import Enum
+except ImportError:
+ Enum = None
+
+
+if sys.version_info >= (3,):
+ import urllib.parse as urlparse
+
+ basestring = str
+else:
+ import urlparse
+
+# Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py
+# fmt: off
+USER_REGEX = re.compile(
+ # start anchor, because fullmatch is not available in python 2.7
+ "(?:"
+ # dot-atom
+ r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+"
+ r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
+ # quoted-string
+ r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|'
+ r"""\\[\001-\011\013\014\016-\177])*"$)"""
+ # end anchor, because fullmatch is not available in python 2.7
+ r")\Z",
+ re.IGNORECASE,
+)
+DOMAIN_REGEX = re.compile(
+ # start anchor, because fullmatch is not available in python 2.7
+ "(?:"
+ # domain
+ r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
+ # tld
+ r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)'
+ # literal form, ipv4 address (SMTP 4.1.3)
+ r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
+ r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$'
+ # end anchor, because fullmatch is not available in python 2.7
+ r")\Z",
+ re.IGNORECASE,
+)
+# fmt: on
+
+__author__ = 'tusharmakkar08'
+
+
+def truth(f: typing.Callable) -> typing.Callable:
+ """Convenience decorator to convert truth functions into validators.
+
+ >>> @truth
+ ... def isdir(v):
+ ... return os.path.isdir(v)
+ >>> validate = Schema(isdir)
+ >>> validate('/')
+ '/'
+ >>> with raises(MultipleInvalid, 'not a valid value'):
+ ... validate('/notavaliddir')
+ """
+
+ @wraps(f)
+ def check(v):
+ t = f(v)
+ if not t:
+ raise ValueError
+ return v
+
+ return check
+
+
+class Coerce(object):
+ """Coerce a value to a type.
+
+ If the type constructor throws a ValueError or TypeError, the value
+ will be marked as Invalid.
+
+ Default behavior:
+
+ >>> validate = Schema(Coerce(int))
+ >>> with raises(MultipleInvalid, 'expected int'):
+ ... validate(None)
+ >>> with raises(MultipleInvalid, 'expected int'):
+ ... validate('foo')
+
+ With custom message:
+
+ >>> validate = Schema(Coerce(int, "moo"))
+ >>> with raises(MultipleInvalid, 'moo'):
+ ... validate('foo')
+ """
+
+ def __init__(
+ self,
+ type: typing.Union[type, typing.Callable],
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ self.type = type
+ self.msg = msg
+ self.type_name = type.__name__
+
+ def __call__(self, v):
+ try:
+ return self.type(v)
+ except (ValueError, TypeError, InvalidOperation):
+ msg = self.msg or ('expected %s' % self.type_name)
+ if not self.msg and Enum and issubclass(self.type, Enum):
+ msg += " or one of %s" % str([e.value for e in self.type])[1:-1]
+ raise CoerceInvalid(msg)
+
+ def __repr__(self):
+ return 'Coerce(%s, msg=%r)' % (self.type_name, self.msg)
+
+
+@message('value was not true', cls=TrueInvalid)
+@truth
+def IsTrue(v):
+ """Assert that a value is true, in the Python sense.
+
+ >>> validate = Schema(IsTrue())
+
+ "In the Python sense" means that implicitly false values, such as empty
+ lists, dictionaries, etc. are treated as "false":
+
+ >>> with raises(MultipleInvalid, "value was not true"):
+ ... validate([])
+ >>> validate([1])
+ [1]
+ >>> with raises(MultipleInvalid, "value was not true"):
+ ... validate(False)
+
+ ...and so on.
+
+ >>> try:
+ ... validate([])
+ ... except MultipleInvalid as e:
+ ... assert isinstance(e.errors[0], TrueInvalid)
+ """
+ return v
+
+
+@message('value was not false', cls=FalseInvalid)
+def IsFalse(v):
+ """Assert that a value is false, in the Python sense.
+
+ (see :func:`IsTrue` for more detail)
+
+ >>> validate = Schema(IsFalse())
+ >>> validate([])
+ []
+ >>> with raises(MultipleInvalid, "value was not false"):
+ ... validate(True)
+
+ >>> try:
+ ... validate(True)
+ ... except MultipleInvalid as e:
+ ... assert isinstance(e.errors[0], FalseInvalid)
+ """
+ if v:
+ raise ValueError
+ return v
+
+
+@message('expected boolean', cls=BooleanInvalid)
+def Boolean(v):
+ """Convert human-readable boolean values to a bool.
+
+ Accepted values are 1, true, yes, on, enable, and their negatives.
+ Non-string values are cast to bool.
+
+ >>> validate = Schema(Boolean())
+ >>> validate(True)
+ True
+ >>> validate("1")
+ True
+ >>> validate("0")
+ False
+ >>> with raises(MultipleInvalid, "expected boolean"):
+ ... validate('moo')
+ >>> try:
+ ... validate('moo')
+ ... except MultipleInvalid as e:
+ ... assert isinstance(e.errors[0], BooleanInvalid)
+ """
+ if isinstance(v, basestring):
+ v = v.lower()
+ if v in ('1', 'true', 'yes', 'on', 'enable'):
+ return True
+ if v in ('0', 'false', 'no', 'off', 'disable'):
+ return False
+ raise ValueError
+ return bool(v)
+
+
+class _WithSubValidators(object):
+ """Base class for validators that use sub-validators.
+
+ Special class to use as a parent class for validators using sub-validators.
+ This class provides the `__voluptuous_compile__` method so the
+ sub-validators are compiled by the parent `Schema`.
+ """
+
+ def __init__(
+ self, *validators, msg=None, required=False, discriminant=None, **kwargs
+ ) -> None:
+ self.validators = validators
+ self.msg = msg
+ self.required = required
+ self.discriminant = discriminant
+
+ def __voluptuous_compile__(self, schema: Schema) -> typing.Callable:
+ self._compiled = []
+ old_required = schema.required
+ self.schema = schema
+ for v in self.validators:
+ schema.required = self.required
+ self._compiled.append(schema._compile(v))
+ schema.required = old_required
+ return self._run
+
+ def _run(self, path: typing.List[typing.Hashable], value):
+ if self.discriminant is not None:
+ self._compiled = [
+ self.schema._compile(v)
+ for v in self.discriminant(value, self.validators)
+ ]
+
+ return self._exec(self._compiled, value, path)
+
+ def __call__(self, v):
+ return self._exec((Schema(val) for val in self.validators), v)
+
+ def __repr__(self):
+ return '%s(%s, msg=%r)' % (
+ self.__class__.__name__,
+ ", ".join(repr(v) for v in self.validators),
+ self.msg,
+ )
+
+ def _exec(
+ self,
+ funcs: typing.Iterable,
+ v,
+ path: typing.Optional[typing.List[typing.Hashable]] = None,
+ ):
+ raise NotImplementedError()
+
+
+class Any(_WithSubValidators):
+ """Use the first validated value.
+
+ :param msg: Message to deliver to user if validation fails.
+ :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
+ :returns: Return value of the first validator that passes.
+
+ >>> validate = Schema(Any('true', 'false',
+ ... All(Any(int, bool), Coerce(bool))))
+ >>> validate('true')
+ 'true'
+ >>> validate(1)
+ True
+ >>> with raises(MultipleInvalid, "not a valid value"):
+ ... validate('moo')
+
+ msg argument is used
+
+ >>> validate = Schema(Any(1, 2, 3, msg="Expected 1 2 or 3"))
+ >>> validate(1)
+ 1
+ >>> with raises(MultipleInvalid, "Expected 1 2 or 3"):
+ ... validate(4)
+ """
+
+ def _exec(self, funcs, v, path=None):
+ error = None
+ for func in funcs:
+ try:
+ if path is None:
+ return func(v)
+ else:
+ return func(path, v)
+ except Invalid as e:
+ if error is None or len(e.path) > len(error.path):
+ error = e
+ else:
+ if error:
+ raise error if self.msg is None else AnyInvalid(self.msg, path=path)
+ raise AnyInvalid(self.msg or 'no valid value found', path=path)
+
+
+# Convenience alias
+Or = Any
+
+
+class Union(_WithSubValidators):
+ """Use the first validated value among those selected by discriminant.
+
+ :param msg: Message to deliver to user if validation fails.
+ :param discriminant(value, validators): Returns the filtered list of validators based on the value.
+ :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
+ :returns: Return value of the first validator that passes.
+
+ >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'},
+ ... discriminant=lambda val, alt: filter(
+ ... lambda v : v['type'] == val['type'] , alt)))
+ >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'}
+ True
+ >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"):
+ ... validate({'type':'b', 'b_val':'5'})
+
+ ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked
+
+ Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']"
+ """
+
+ def _exec(self, funcs, v, path=None):
+ error = None
+ for func in funcs:
+ try:
+ if path is None:
+ return func(v)
+ else:
+ return func(path, v)
+ except Invalid as e:
+ if error is None or len(e.path) > len(error.path):
+ error = e
+ else:
+ if error:
+ raise error if self.msg is None else AnyInvalid(self.msg, path=path)
+ raise AnyInvalid(self.msg or 'no valid value found', path=path)
+
+
+# Convenience alias
+Switch = Union
+
+
+class All(_WithSubValidators):
+ """Value must pass all validators.
+
+ The output of each validator is passed as input to the next.
+
+ :param msg: Message to deliver to user if validation fails.
+ :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
+
+ >>> validate = Schema(All('10', Coerce(int)))
+ >>> validate('10')
+ 10
+ """
+
+ def _exec(self, funcs, v, path=None):
+ try:
+ for func in funcs:
+ if path is None:
+ v = func(v)
+ else:
+ v = func(path, v)
+ except Invalid as e:
+ raise e if self.msg is None else AllInvalid(self.msg, path=path)
+ return v
+
+
+# Convenience alias
+And = All
+
+
+class Match(object):
+ """Value must be a string that matches the regular expression.
+
+ >>> validate = Schema(Match(r'^0x[A-F0-9]+$'))
+ >>> validate('0x123EF4')
+ '0x123EF4'
+ >>> with raises(MultipleInvalid, 'does not match regular expression ^0x[A-F0-9]+$'):
+ ... validate('123EF4')
+
+ >>> with raises(MultipleInvalid, 'expected string or buffer'):
+ ... validate(123)
+
+ Pattern may also be a compiled regular expression:
+
+ >>> validate = Schema(Match(re.compile(r'0x[A-F0-9]+', re.I)))
+ >>> validate('0x123ef4')
+ '0x123ef4'
+ """
+
+ def __init__(
+ self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None
+ ) -> None:
+ if isinstance(pattern, basestring):
+ pattern = re.compile(pattern)
+ self.pattern = pattern
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ match = self.pattern.match(v)
+ except TypeError:
+ raise MatchInvalid("expected string or buffer")
+ if not match:
+ raise MatchInvalid(
+ self.msg
+ or 'does not match regular expression {}'.format(self.pattern.pattern)
+ )
+ return v
+
+ def __repr__(self):
+ return 'Match(%r, msg=%r)' % (self.pattern.pattern, self.msg)
+
+
+class Replace(object):
+ """Regex substitution.
+
+ >>> validate = Schema(All(Replace('you', 'I'),
+ ... Replace('hello', 'goodbye')))
+ >>> validate('you say hello')
+ 'I say goodbye'
+ """
+
+ def __init__(
+ self,
+ pattern: typing.Union[re.Pattern, str],
+ substitution: str,
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ if isinstance(pattern, basestring):
+ pattern = re.compile(pattern)
+ self.pattern = pattern
+ self.substitution = substitution
+ self.msg = msg
+
+ def __call__(self, v):
+ return self.pattern.sub(self.substitution, v)
+
+ def __repr__(self):
+ return 'Replace(%r, %r, msg=%r)' % (
+ self.pattern.pattern,
+ self.substitution,
+ self.msg,
+ )
+
+
+def _url_validation(v: str) -> urlparse.ParseResult:
+ parsed = urlparse.urlparse(v)
+ if not parsed.scheme or not parsed.netloc:
+ raise UrlInvalid("must have a URL scheme and host")
+ return parsed
+
+
+@message('expected an email address', cls=EmailInvalid)
+def Email(v):
+ """Verify that the value is an email address or not.
+
+ >>> s = Schema(Email())
+ >>> with raises(MultipleInvalid, 'expected an email address'):
+ ... s("a.com")
+ >>> with raises(MultipleInvalid, 'expected an email address'):
+ ... s("a@.com")
+ >>> with raises(MultipleInvalid, 'expected an email address'):
+ ... s("a@.com")
+ >>> s('t@x.com')
+ 't@x.com'
+ """
+ try:
+ if not v or "@" not in v:
+ raise EmailInvalid("Invalid email address")
+ user_part, domain_part = v.rsplit('@', 1)
+
+ if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)):
+ raise EmailInvalid("Invalid email address")
+ return v
+ except: # noqa: E722
+ raise ValueError
+
+
+@message('expected a fully qualified domain name URL', cls=UrlInvalid)
+def FqdnUrl(v):
+ """Verify that the value is a fully qualified domain name URL.
+
+ >>> s = Schema(FqdnUrl())
+ >>> with raises(MultipleInvalid, 'expected a fully qualified domain name URL'):
+ ... s("http://localhost/")
+ >>> s('http://w3.org')
+ 'http://w3.org'
+ """
+ try:
+ parsed_url = _url_validation(v)
+ if "." not in parsed_url.netloc:
+ raise UrlInvalid("must have a domain name in URL")
+ return v
+ except: # noqa: E722
+ raise ValueError
+
+
+@message('expected a URL', cls=UrlInvalid)
+def Url(v):
+ """Verify that the value is a URL.
+
+ >>> s = Schema(Url())
+ >>> with raises(MultipleInvalid, 'expected a URL'):
+ ... s(1)
+ >>> s('http://w3.org')
+ 'http://w3.org'
+ """
+ try:
+ _url_validation(v)
+ return v
+ except: # noqa: E722
+ raise ValueError
+
+
+@message('Not a file', cls=FileInvalid)
+@truth
+def IsFile(v):
+ """Verify the file exists.
+
+ >>> os.path.basename(IsFile()(__file__)).startswith('validators.py')
+ True
+ >>> with raises(FileInvalid, 'Not a file'):
+ ... IsFile()("random_filename_goes_here.py")
+ >>> with raises(FileInvalid, 'Not a file'):
+ ... IsFile()(None)
+ """
+ try:
+ if v:
+ v = str(v)
+ return os.path.isfile(v)
+ else:
+ raise FileInvalid('Not a file')
+ except TypeError:
+ raise FileInvalid('Not a file')
+
+
+@message('Not a directory', cls=DirInvalid)
+@truth
+def IsDir(v):
+ """Verify the directory exists.
+
+ >>> IsDir()('/')
+ '/'
+ >>> with raises(DirInvalid, 'Not a directory'):
+ ... IsDir()(None)
+ """
+ try:
+ if v:
+ v = str(v)
+ return os.path.isdir(v)
+ else:
+ raise DirInvalid("Not a directory")
+ except TypeError:
+ raise DirInvalid("Not a directory")
+
+
+@message('path does not exist', cls=PathInvalid)
+@truth
+def PathExists(v):
+ """Verify the path exists, regardless of its type.
+
+ >>> os.path.basename(PathExists()(__file__)).startswith('validators.py')
+ True
+ >>> with raises(Invalid, 'path does not exist'):
+ ... PathExists()("random_filename_goes_here.py")
+ >>> with raises(PathInvalid, 'Not a Path'):
+ ... PathExists()(None)
+ """
+ try:
+ if v:
+ v = str(v)
+ return os.path.exists(v)
+ else:
+ raise PathInvalid("Not a Path")
+ except TypeError:
+ raise PathInvalid("Not a Path")
+
+
+def Maybe(validator: Schemable, msg: typing.Optional[str] = None):
+ """Validate that the object matches given validator or is None.
+
+ :raises Invalid: If the value does not match the given validator and is not
+ None.
+
+ >>> s = Schema(Maybe(int))
+ >>> s(10)
+ 10
+ >>> with raises(Invalid):
+ ... s("string")
+
+ """
+ return Any(None, validator, msg=msg)
+
+
+class Range(object):
+ """Limit a value to a range.
+
+ Either min or max may be omitted.
+ Either min or max can be excluded from the range of accepted values.
+
+ :raises Invalid: If the value is outside the range.
+
+ >>> s = Schema(Range(min=1, max=10, min_included=False))
+ >>> s(5)
+ 5
+ >>> s(10)
+ 10
+ >>> with raises(MultipleInvalid, 'value must be at most 10'):
+ ... s(20)
+ >>> with raises(MultipleInvalid, 'value must be higher than 1'):
+ ... s(1)
+ >>> with raises(MultipleInvalid, 'value must be lower than 10'):
+ ... Schema(Range(max=10, max_included=False))(20)
+ """
+
+ def __init__(
+ self,
+ min: SupportsAllComparisons | None = None,
+ max: SupportsAllComparisons | None = None,
+ min_included: bool = True,
+ max_included: bool = True,
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ self.min = min
+ self.max = max
+ self.min_included = min_included
+ self.max_included = max_included
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ if self.min_included:
+ if self.min is not None and not v >= self.min:
+ raise RangeInvalid(
+ self.msg or 'value must be at least %s' % self.min
+ )
+ else:
+ if self.min is not None and not v > self.min:
+ raise RangeInvalid(
+ self.msg or 'value must be higher than %s' % self.min
+ )
+ if self.max_included:
+ if self.max is not None and not v <= self.max:
+ raise RangeInvalid(
+ self.msg or 'value must be at most %s' % self.max
+ )
+ else:
+ if self.max is not None and not v < self.max:
+ raise RangeInvalid(
+ self.msg or 'value must be lower than %s' % self.max
+ )
+
+ return v
+
+ # Objects that lack a partial ordering, e.g. None or strings will raise TypeError
+ except TypeError:
+ raise RangeInvalid(
+ self.msg or 'invalid value or type (must have a partial ordering)'
+ )
+
+ def __repr__(self):
+ return 'Range(min=%r, max=%r, min_included=%r, max_included=%r, msg=%r)' % (
+ self.min,
+ self.max,
+ self.min_included,
+ self.max_included,
+ self.msg,
+ )
+
+
+class Clamp(object):
+ """Clamp a value to a range.
+
+ Either min or max may be omitted.
+
+ >>> s = Schema(Clamp(min=0, max=1))
+ >>> s(0.5)
+ 0.5
+ >>> s(5)
+ 1
+ >>> s(-1)
+ 0
+ """
+
+ def __init__(
+ self,
+ min: SupportsAllComparisons | None = None,
+ max: SupportsAllComparisons | None = None,
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ self.min = min
+ self.max = max
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ if self.min is not None and v < self.min:
+ v = self.min
+ if self.max is not None and v > self.max:
+ v = self.max
+ return v
+
+ # Objects that lack a partial ordering, e.g. None or strings will raise TypeError
+ except TypeError:
+ raise RangeInvalid(
+ self.msg or 'invalid value or type (must have a partial ordering)'
+ )
+
+ def __repr__(self):
+ return 'Clamp(min=%s, max=%s)' % (self.min, self.max)
+
+
+class Length(object):
+ """The length of a value must be in a certain range."""
+
+ def __init__(
+ self,
+ min: SupportsAllComparisons | None = None,
+ max: SupportsAllComparisons | None = None,
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ self.min = min
+ self.max = max
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ if self.min is not None and len(v) < self.min:
+ raise LengthInvalid(
+ self.msg or 'length of value must be at least %s' % self.min
+ )
+ if self.max is not None and len(v) > self.max:
+ raise LengthInvalid(
+ self.msg or 'length of value must be at most %s' % self.max
+ )
+ return v
+
+ # Objects that have no length e.g. None or strings will raise TypeError
+ except TypeError:
+ raise RangeInvalid(self.msg or 'invalid value or type')
+
+ def __repr__(self):
+ return 'Length(min=%s, max=%s)' % (self.min, self.max)
+
+
+class Datetime(object):
+ """Validate that the value matches the datetime format."""
+
+ DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
+
+ def __init__(
+ self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None
+ ) -> None:
+ self.format = format or self.DEFAULT_FORMAT
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ datetime.datetime.strptime(v, self.format)
+ except (TypeError, ValueError):
+ raise DatetimeInvalid(
+ self.msg or 'value does not match expected format %s' % self.format
+ )
+ return v
+
+ def __repr__(self):
+ return 'Datetime(format=%s)' % self.format
+
+
+class Date(Datetime):
+ """Validate that the value matches the date format."""
+
+ DEFAULT_FORMAT = '%Y-%m-%d'
+
+ def __call__(self, v):
+ try:
+ datetime.datetime.strptime(v, self.format)
+ except (TypeError, ValueError):
+ raise DateInvalid(
+ self.msg or 'value does not match expected format %s' % self.format
+ )
+ return v
+
+ def __repr__(self):
+ return 'Date(format=%s)' % self.format
+
+
+class In(object):
+ """Validate that a value is in a collection."""
+
+ def __init__(
+ self,
+ container: typing.Container | typing.Iterable,
+ msg: typing.Optional[str] = None,
+ ) -> None:
+ self.container = container
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ check = v not in self.container
+ except TypeError:
+ check = True
+ if check:
+ try:
+ raise InInvalid(
+ self.msg or f'value must be one of {sorted(self.container)}'
+ )
+ except TypeError:
+ raise InInvalid(
+ self.msg
+ or f'value must be one of {sorted(self.container, key=str)}'
+ )
+ return v
+
+ def __repr__(self):
+ return 'In(%s)' % (self.container,)
+
+
+class NotIn(object):
+ """Validate that a value is not in a collection."""
+
+ def __init__(
+ self, container: typing.Iterable, msg: typing.Optional[str] = None
+ ) -> None:
+ self.container = container
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ check = v in self.container
+ except TypeError:
+ check = True
+ if check:
+ try:
+ raise NotInInvalid(
+ self.msg or f'value must not be one of {sorted(self.container)}'
+ )
+ except TypeError:
+ raise NotInInvalid(
+ self.msg
+ or f'value must not be one of {sorted(self.container, key=str)}'
+ )
+ return v
+
+ def __repr__(self):
+ return 'NotIn(%s)' % (self.container,)
+
+
+class Contains(object):
+ """Validate that the given schema element is in the sequence being validated.
+
+ >>> s = Contains(1)
+ >>> s([3, 2, 1])
+ [3, 2, 1]
+ >>> with raises(ContainsInvalid, 'value is not allowed'):
+ ... s([3, 2])
+ """
+
+ def __init__(self, item, msg: typing.Optional[str] = None) -> None:
+ self.item = item
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ check = self.item not in v
+ except TypeError:
+ check = True
+ if check:
+ raise ContainsInvalid(self.msg or 'value is not allowed')
+ return v
+
+ def __repr__(self):
+ return 'Contains(%s)' % (self.item,)
+
+
+class ExactSequence(object):
+ """Matches each element in a sequence against the corresponding element in
+ the validators.
+
+ :param msg: Message to deliver to user if validation fails.
+ :param kwargs: All other keyword arguments are passed to the sub-schema
+ constructors.
+
+ >>> from voluptuous import Schema, ExactSequence
+ >>> validate = Schema(ExactSequence([str, int, list, list]))
+ >>> validate(['hourly_report', 10, [], []])
+ ['hourly_report', 10, [], []]
+ >>> validate(('hourly_report', 10, [], []))
+ ('hourly_report', 10, [], [])
+ """
+
+ def __init__(
+ self,
+ validators: typing.Iterable[Schemable],
+ msg: typing.Optional[str] = None,
+ **kwargs,
+ ) -> None:
+ self.validators = validators
+ self.msg = msg
+ self._schemas = [Schema(val, **kwargs) for val in validators]
+
+ def __call__(self, v):
+ if not isinstance(v, (list, tuple)) or len(v) != len(self._schemas):
+ raise ExactSequenceInvalid(self.msg)
+ try:
+ v = type(v)(schema(x) for x, schema in zip(v, self._schemas))
+ except Invalid as e:
+ raise e if self.msg is None else ExactSequenceInvalid(self.msg)
+ return v
+
+ def __repr__(self):
+ return 'ExactSequence([%s])' % ", ".join(repr(v) for v in self.validators)
+
+
+class Unique(object):
+ """Ensure an iterable does not contain duplicate items.
+
+ Only iterables convertible to a set are supported (native types and
+ objects with correct __eq__).
+
+ JSON does not support set, so they need to be presented as arrays.
+ Unique allows ensuring that such array does not contain dupes.
+
+ >>> s = Schema(Unique())
+ >>> s([])
+ []
+ >>> s([1, 2])
+ [1, 2]
+ >>> with raises(Invalid, 'contains duplicate items: [1]'):
+ ... s([1, 1, 2])
+ >>> with raises(Invalid, "contains duplicate items: ['one']"):
+ ... s(['one', 'two', 'one'])
+ >>> with raises(Invalid, regex="^contains unhashable elements: "):
+ ... s([set([1, 2]), set([3, 4])])
+ >>> s('abc')
+ 'abc'
+ >>> with raises(Invalid, regex="^contains duplicate items: "):
+ ... s('aabbc')
+ """
+
+ def __init__(self, msg: typing.Optional[str] = None) -> None:
+ self.msg = msg
+
+ def __call__(self, v):
+ try:
+ set_v = set(v)
+ except TypeError as e:
+ raise TypeInvalid(self.msg or 'contains unhashable elements: {0}'.format(e))
+ if len(set_v) != len(v):
+ seen = set()
+ dupes = list(set(x for x in v if x in seen or seen.add(x)))
+ raise Invalid(self.msg or 'contains duplicate items: {0}'.format(dupes))
+ return v
+
+ def __repr__(self):
+ return 'Unique()'
+
+
+class Equal(object):
+ """Ensure that value matches target.
+
+ >>> s = Schema(Equal(1))
+ >>> s(1)
+ 1
+ >>> with raises(Invalid):
+ ... s(2)
+
+ Validators are not supported, match must be exact:
+
+ >>> s = Schema(Equal(str))
+ >>> with raises(Invalid):
+ ... s('foo')
+ """
+
+ def __init__(self, target, msg: typing.Optional[str] = None) -> None:
+ self.target = target
+ self.msg = msg
+
+ def __call__(self, v):
+ if v != self.target:
+ raise Invalid(
+ self.msg
+ or 'Values are not equal: value:{} != target:{}'.format(v, self.target)
+ )
+ return v
+
+ def __repr__(self):
+ return 'Equal({})'.format(self.target)
+
+
+class Unordered(object):
+ """Ensures sequence contains values in unspecified order.
+
+ >>> s = Schema(Unordered([2, 1]))
+ >>> s([2, 1])
+ [2, 1]
+ >>> s([1, 2])
+ [1, 2]
+ >>> s = Schema(Unordered([str, int]))
+ >>> s(['foo', 1])
+ ['foo', 1]
+ >>> s([1, 'foo'])
+ [1, 'foo']
+ """
+
+ def __init__(
+ self,
+ validators: typing.Iterable[Schemable],
+ msg: typing.Optional[str] = None,
+ **kwargs,
+ ) -> None:
+ self.validators = validators
+ self.msg = msg
+ self._schemas = [Schema(val, **kwargs) for val in validators]
+
+ def __call__(self, v):
+ if not isinstance(v, (list, tuple)):
+ raise Invalid(self.msg or 'Value {} is not sequence!'.format(v))
+
+ if len(v) != len(self._schemas):
+ raise Invalid(
+ self.msg
+ or 'List lengths differ, value:{} != target:{}'.format(
+ len(v), len(self._schemas)
+ )
+ )
+
+ consumed = set()
+ missing = []
+ for index, value in enumerate(v):
+ found = False
+ for i, s in enumerate(self._schemas):
+ if i in consumed:
+ continue
+ try:
+ s(value)
+ except Invalid:
+ pass
+ else:
+ found = True
+ consumed.add(i)
+ break
+ if not found:
+ missing.append((index, value))
+
+ if len(missing) == 1:
+ el = missing[0]
+ raise Invalid(
+ self.msg
+ or 'Element #{} ({}) is not valid against any validator'.format(
+ el[0], el[1]
+ )
+ )
+ elif missing:
+ raise MultipleInvalid(
+ [
+ Invalid(
+ self.msg
+ or 'Element #{} ({}) is not valid against any validator'.format(
+ el[0], el[1]
+ )
+ )
+ for el in missing
+ ]
+ )
+ return v
+
+ def __repr__(self):
+ return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators))
+
+
+class Number(object):
+ """
+ Verify the number of digits that are present in the number(Precision),
+ and the decimal places(Scale).
+
+ :raises Invalid: If the value does not match the provided Precision and Scale.
+
+ >>> schema = Schema(Number(precision=6, scale=2))
+ >>> schema('1234.01')
+ '1234.01'
+ >>> schema = Schema(Number(precision=6, scale=2, yield_decimal=True))
+ >>> schema('1234.01')
+ Decimal('1234.01')
+ """
+
+ def __init__(
+ self,
+ precision: typing.Optional[int] = None,
+ scale: typing.Optional[int] = None,
+ msg: typing.Optional[str] = None,
+ yield_decimal: bool = False,
+ ) -> None:
+ self.precision = precision
+ self.scale = scale
+ self.msg = msg
+ self.yield_decimal = yield_decimal
+
+ def __call__(self, v):
+ """
+ :param v: is a number enclosed with string
+ :return: Decimal number
+ """
+ precision, scale, decimal_num = self._get_precision_scale(v)
+
+ if (
+ self.precision is not None
+ and self.scale is not None
+ and precision != self.precision
+ and scale != self.scale
+ ):
+ raise Invalid(
+ self.msg
+ or "Precision must be equal to %s, and Scale must be equal to %s"
+ % (self.precision, self.scale)
+ )
+ else:
+ if self.precision is not None and precision != self.precision:
+ raise Invalid(
+ self.msg or "Precision must be equal to %s" % self.precision
+ )
+
+ if self.scale is not None and scale != self.scale:
+ raise Invalid(self.msg or "Scale must be equal to %s" % self.scale)
+
+ if self.yield_decimal:
+ return decimal_num
+ else:
+ return v
+
+ def __repr__(self):
+ return 'Number(precision=%s, scale=%s, msg=%s)' % (
+ self.precision,
+ self.scale,
+ self.msg,
+ )
+
+ def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]:
+ """
+ :param number:
+ :return: tuple(precision, scale, decimal_number)
+ """
+ try:
+ decimal_num = Decimal(number)
+ except InvalidOperation:
+ raise Invalid(self.msg or 'Value must be a number enclosed with string')
+
+ exp = decimal_num.as_tuple().exponent
+ if isinstance(exp, int):
+ return (len(decimal_num.as_tuple().digits), -exp, decimal_num)
+ else:
+ # TODO: handle infinity and NaN
+ # raise Invalid(self.msg or 'Value has no precision')
+ raise TypeError("infinity and NaN have no precision")
+
+
+class SomeOf(_WithSubValidators):
+ """Value must pass at least some validations, determined by the given parameter.
+ Optionally, number of passed validations can be capped.
+
+ The output of each validator is passed as input to the next.
+
+ :param min_valid: Minimum number of valid schemas.
+ :param validators: List of schemas or validators to match input against.
+ :param max_valid: Maximum number of valid schemas.
+ :param msg: Message to deliver to user if validation fails.
+ :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
+
+ :raises NotEnoughValid: If the minimum number of validations isn't met.
+ :raises TooManyValid: If the maximum number of validations is exceeded.
+
+ >>> validate = Schema(SomeOf(min_valid=2, validators=[Range(1, 5), Any(float, int), 6.6]))
+ >>> validate(6.6)
+ 6.6
+ >>> validate(3)
+ 3
+ >>> with raises(MultipleInvalid, 'value must be at most 5, not a valid value'):
+ ... validate(6.2)
+ """
+
+ def __init__(
+ self,
+ validators: typing.List[Schemable],
+ min_valid: typing.Optional[int] = None,
+ max_valid: typing.Optional[int] = None,
+ **kwargs,
+ ) -> None:
+ assert min_valid is not None or max_valid is not None, (
+ 'when using "%s" you should specify at least one of min_valid and max_valid'
+ % (type(self).__name__,)
+ )
+ self.min_valid = min_valid or 0
+ self.max_valid = max_valid or len(validators)
+ super(SomeOf, self).__init__(*validators, **kwargs)
+
+ def _exec(self, funcs, v, path=None):
+ errors = []
+ funcs = list(funcs)
+ for func in funcs:
+ try:
+ if path is None:
+ v = func(v)
+ else:
+ v = func(path, v)
+ except Invalid as e:
+ errors.append(e)
+
+ passed_count = len(funcs) - len(errors)
+ if self.min_valid <= passed_count <= self.max_valid:
+ return v
+
+ msg = self.msg
+ if not msg:
+ msg = ', '.join(map(str, errors))
+
+ if passed_count > self.max_valid:
+ raise TooManyValid(msg)
+ raise NotEnoughValid(msg)
+
+ def __repr__(self):
+ return 'SomeOf(min_valid=%s, validators=[%s], max_valid=%s, msg=%r)' % (
+ self.min_valid,
+ ", ".join(repr(v) for v in self.validators),
+ self.max_valid,
+ self.msg,
+ )
\ No newline at end of file
diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml
index c58fc49c67aaf..bedc90943e096 100644
--- a/features/openhab-addons/src/main/resources/footer.xml
+++ b/features/openhab-addons/src/main/resources/footer.xml
@@ -24,24 +24,24 @@
openhab-runtime-base
openhab-transport-mqtt
openhab.tp-commons-net
- mvn:org.openhab.osgiify/org.graalvm.llvm.llvm-api/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-embedding/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-language/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.python.python-resources/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.regex.regex/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.collections/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.json/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-api/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-nfi/24.2.1
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/24.2.1
+ mvn:org.openhab.osgiify/org.graalvm.llvm.llvm-api/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-embedding/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-language/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.python.python-resources/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.regex.regex/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.collections/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.sdk.word/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.json/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-api/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-nfi/25.0.1
+ mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/25.0.1
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.awtrixlight/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.espmilighthub/${project.version}