PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/schema.py |
""" :codeauthor: Pedro Algarvio (pedro@algarvio.me) :codeauthor: Alexandru Bleotu (alexandru.bleotu@morganstanley.com) salt.utils.schema ~~~~~~~~~~~~~~~~~ Object Oriented Configuration - JSON Schema compatible generator This code was inspired by `jsl`__, "A Python DSL for describing JSON schemas". .. __: https://jsl.readthedocs.io/ A configuration document or configuration document section is defined using the py:class:`Schema`, the configuration items are defined by any of the subclasses of py:class:`BaseSchemaItem` as attributes of a subclass of py:class:`Schema` class. A more complex configuration document (containing a defininitions section) is defined using the py:class:`DefinitionsSchema`. This type of schema supports having complex configuration items as attributes (defined extending the py:class:`ComplexSchemaItem`). These items have other configuration items (complex or not) as attributes, allowing to verify more complex JSON data structures As an example: .. code-block:: python class HostConfig(Schema): title = 'Host Configuration' description = 'This is the host configuration' host = StringItem( 'Host', 'The looong host description', default=None, minimum=1 ) port = NumberItem( description='The port number', default=80, required=False, minimum=0, inclusiveMinimum=False, maximum=65535 ) The serialized version of the above configuration definition is: .. code-block:: python >>> print(HostConfig.serialize()) OrderedDict([ ('$schema', 'http://json-schema.org/draft-04/schema#'), ('title', 'Host Configuration'), ('description', 'This is the host configuration'), ('type', 'object'), ('properties', OrderedDict([ ('host', {'minimum': 1, 'type': 'string', 'description': 'The looong host description', 'title': 'Host'}), ('port', {'description': 'The port number', 'default': 80, 'inclusiveMinimum': False, 'maximum': 65535, 'minimum': 0, 'type': 'number'}) ])), ('required', ['host']), ('x-ordering', ['host', 'port']), ('additionalProperties', True)] ) >>> print(salt.utils.json.dumps(HostConfig.serialize(), indent=2)) { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Host Configuration", "description": "This is the host configuration", "type": "object", "properties": { "host": { "minimum": 1, "type": "string", "description": "The looong host description", "title": "Host" }, "port": { "description": "The port number", "default": 80, "inclusiveMinimum": false, "maximum": 65535, "minimum": 0, "type": "number" } }, "required": [ "host" ], "x-ordering": [ "host", "port" ], "additionalProperties": false } The serialized version of the configuration block can be used to validate a configuration dictionary using the `python jsonschema library`__. .. __: https://pypi.python.org/pypi/jsonschema .. code-block:: python >>> import jsonschema >>> jsonschema.validate({'host': 'localhost', 'port': 80}, HostConfig.serialize()) >>> jsonschema.validate({'host': 'localhost', 'port': -1}, HostConfig.serialize()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/site-packages/jsonschema/validators.py", line 478, in validate cls(schema, *args, **kwargs).validate(instance) File "/usr/lib/python2.7/site-packages/jsonschema/validators.py", line 123, in validate raise error jsonschema.exceptions.ValidationError: -1 is less than the minimum of 0 Failed validating 'minimum' in schema['properties']['port']: {'default': 80, 'description': 'The port number', 'inclusiveMinimum': False, 'maximum': 65535, 'minimum': 0, 'type': 'number'} On instance['port']: -1 >>> A configuration document can even be split into configuration sections. Let's reuse the above ``HostConfig`` class and include it in a configuration block: .. code-block:: python class LoggingConfig(Schema): title = 'Logging Configuration' description = 'This is the logging configuration' log_level = StringItem( 'Logging Level', 'The logging level', default='debug', minimum=1 ) class MyConfig(Schema): title = 'My Config' description = 'This my configuration' hostconfig = HostConfig() logconfig = LoggingConfig() The JSON Schema string version of the above is: .. code-block:: python >>> print salt.utils.json.dumps(MyConfig.serialize(), indent=4) { "$schema": "http://json-schema.org/draft-04/schema#", "title": "My Config", "description": "This my configuration", "type": "object", "properties": { "hostconfig": { "id": "https://non-existing.saltstack.com/schemas/hostconfig.json#", "title": "Host Configuration", "description": "This is the host configuration", "type": "object", "properties": { "host": { "minimum": 1, "type": "string", "description": "The looong host description", "title": "Host" }, "port": { "description": "The port number", "default": 80, "inclusiveMinimum": false, "maximum": 65535, "minimum": 0, "type": "number" } }, "required": [ "host" ], "x-ordering": [ "host", "port" ], "additionalProperties": false }, "logconfig": { "id": "https://non-existing.saltstack.com/schemas/logconfig.json#", "title": "Logging Configuration", "description": "This is the logging configuration", "type": "object", "properties": { "log_level": { "default": "debug", "minimum": 1, "type": "string", "description": "The logging level", "title": "Logging Level" } }, "required": [ "log_level" ], "x-ordering": [ "log_level" ], "additionalProperties": false } }, "additionalProperties": false } >>> import jsonschema >>> jsonschema.validate( {'hostconfig': {'host': 'localhost', 'port': 80}, 'logconfig': {'log_level': 'debug'}}, MyConfig.serialize()) >>> jsonschema.validate( {'hostconfig': {'host': 'localhost', 'port': -1}, 'logconfig': {'log_level': 'debug'}}, MyConfig.serialize()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/site-packages/jsonschema/validators.py", line 478, in validate cls(schema, *args, **kwargs).validate(instance) File "/usr/lib/python2.7/site-packages/jsonschema/validators.py", line 123, in validate raise error jsonschema.exceptions.ValidationError: -1 is less than the minimum of 0 Failed validating 'minimum' in schema['properties']['hostconfig']['properties']['port']: {'default': 80, 'description': 'The port number', 'inclusiveMinimum': False, 'maximum': 65535, 'minimum': 0, 'type': 'number'} On instance['hostconfig']['port']: -1 >>> If however, you just want to use the configuration blocks for readability and do not desire the nested dictionaries serialization, you can pass ``flatten=True`` when defining a configuration section as a configuration subclass attribute: .. code-block:: python class MyConfig(Schema): title = 'My Config' description = 'This my configuration' hostconfig = HostConfig(flatten=True) logconfig = LoggingConfig(flatten=True) The JSON Schema string version of the above is: .. code-block:: python >>> print(salt.utils.json.dumps(MyConfig, indent=4)) { "$schema": "http://json-schema.org/draft-04/schema#", "title": "My Config", "description": "This my configuration", "type": "object", "properties": { "host": { "minimum": 1, "type": "string", "description": "The looong host description", "title": "Host" }, "port": { "description": "The port number", "default": 80, "inclusiveMinimum": false, "maximum": 65535, "minimum": 0, "type": "number" }, "log_level": { "default": "debug", "minimum": 1, "type": "string", "description": "The logging level", "title": "Logging Level" } }, "x-ordering": [ "host", "port", "log_level" ], "additionalProperties": false } """ import inspect import textwrap import salt.utils.args # import salt.utils.yaml from salt.utils.odict import OrderedDict BASE_SCHEMA_URL = "https://non-existing.saltstack.com/schemas" RENDER_COMMENT_YAML_MAX_LINE_LENGTH = 80 class NullSentinel: """ A class which instance represents a null value. Allows specifying fields with a default value of null. """ def __bool__(self): return False __nonzero__ = __bool__ Null = NullSentinel() """ A special value that can be used to set the default value of a field to null. """ # make sure nobody creates another Null value def _failing_new(*args, **kwargs): raise TypeError("Can't create another NullSentinel instance") NullSentinel.__new__ = staticmethod(_failing_new) del _failing_new class SchemaMeta(type): @classmethod def __prepare__(mcs, name, bases): return OrderedDict() def __new__(mcs, name, bases, attrs): # Mark the instance as a configuration document/section attrs["__config__"] = True attrs["__flatten__"] = False attrs["__config_name__"] = None # Let's record the configuration items/sections items = {} sections = {} order = [] # items from parent classes for base in reversed(bases): if hasattr(base, "_items"): items.update(base._items) if hasattr(base, "_sections"): sections.update(base._sections) if hasattr(base, "_order"): order.extend(base._order) # Iterate through attrs to discover items/config sections for key, value in attrs.items(): entry_name = None if not hasattr(value, "__item__") and not hasattr(value, "__config__"): continue if hasattr(value, "__item__"): # the value is an item instance if hasattr(value, "title") and value.title is None: # It's an item instance without a title, make the title # its name value.title = key entry_name = value.__item_name__ or key items[entry_name] = value if hasattr(value, "__config__"): entry_name = value.__config_name__ or key sections[entry_name] = value order.append(entry_name) attrs["_order"] = order attrs["_items"] = items attrs["_sections"] = sections return type.__new__(mcs, name, bases, attrs) def __call__(cls, flatten=False, allow_additional_items=False, **kwargs): instance = object.__new__(cls) instance.__config_name__ = kwargs.pop("name", None) if flatten is True: # This configuration block is to be treated as a part of the # configuration for which it was defined as an attribute, not as # its own sub configuration instance.__flatten__ = True if allow_additional_items is True: # The configuration block only accepts the configuration items # which are defined on the class. On additional items, validation # with jsonschema will fail instance.__allow_additional_items__ = True instance.__init__(**kwargs) return instance class BaseSchemaItemMeta(type): """ Config item metaclass to "tag" the class as a configuration item """ @classmethod def __prepare__(mcs, name, bases): return OrderedDict() def __new__(mcs, name, bases, attrs): # Register the class as an item class attrs["__item__"] = True attrs["__item_name__"] = None # Instantiate an empty list to store the config item attribute names attributes = [] for base in reversed(bases): try: base_attributes = getattr(base, "_attributes", []) if base_attributes: attributes.extend(base_attributes) # Extend the attributes with the base argspec argument names # but skip "self" for argname in salt.utils.args.get_function_argspec(base.__init__).args: if argname == "self" or argname in attributes: continue if argname == "name": continue attributes.append(argname) except TypeError: # On the base object type, __init__ is just a wrapper which # triggers a TypeError when we're trying to find out its # argspec continue attrs["_attributes"] = attributes return type.__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): # Create the instance class instance = object.__new__(cls) if args: raise RuntimeError( "Please pass all arguments as named arguments. Un-named " "arguments are not supported" ) for key in kwargs.copy(): # Store the kwarg keys as the instance attributes for the # serialization step if key == "name": # This is the item name to override the class attribute name instance.__item_name__ = kwargs.pop(key) continue if key not in instance._attributes: instance._attributes.append(key) # Init the class instance.__init__(*args, **kwargs) # Validate the instance after initialization for base in reversed(inspect.getmro(cls)): validate_attributes = getattr(base, "__validate_attributes__", None) if validate_attributes: if ( instance.__validate_attributes__.__func__.__code__ is not validate_attributes.__code__ ): # The method was overridden, run base.__validate_attributes__ function base.__validate_attributes__(instance) # Finally, run the instance __validate_attributes__ function instance.__validate_attributes__() # Return the initialized class return instance class Schema(metaclass=SchemaMeta): """ Configuration definition class """ # Define some class level attributes to make PyLint happier title = None description = None _items = _sections = _order = None __flatten__ = False __allow_additional_items__ = False @classmethod def serialize(cls, id_=None): # The order matters serialized = OrderedDict() if id_ is not None: # This is meant as a configuration section, sub json schema serialized["id"] = f"{BASE_SCHEMA_URL}/{id_}.json#" else: # Main configuration block, json schema serialized["$schema"] = "http://json-schema.org/draft-04/schema#" if cls.title is not None: serialized["title"] = cls.title if cls.description is not None: if cls.description == cls.__doc__: serialized["description"] = textwrap.dedent(cls.description).strip() else: serialized["description"] = cls.description required = [] ordering = [] serialized["type"] = "object" properties = OrderedDict() cls.after_items_update = [] for name in cls._order: # pylint: disable=E1133 skip_order = False item_name = None if name in cls._sections: # pylint: disable=E1135 section = cls._sections[name] serialized_section = section.serialize( None if section.__flatten__ is True else name ) if section.__flatten__ is True: # Flatten the configuration section into the parent # configuration properties.update(serialized_section["properties"]) if "x-ordering" in serialized_section: ordering.extend(serialized_section["x-ordering"]) if "required" in serialized_section: required.extend(serialized_section["required"]) if hasattr(section, "after_items_update"): cls.after_items_update.extend(section.after_items_update) skip_order = True else: # Store it as a configuration section properties[name] = serialized_section if name in cls._items: # pylint: disable=E1135 config = cls._items[name] item_name = config.__item_name__ or name # Handle the configuration items defined in the class instance if config.__flatten__ is True: serialized_config = config.serialize() cls.after_items_update.append(serialized_config) skip_order = True else: properties[item_name] = config.serialize() if config.required: # If it's a required item, add it to the required list required.append(item_name) if skip_order is False: # Store the order of the item if item_name is not None: if item_name not in ordering: ordering.append(item_name) else: if name not in ordering: ordering.append(name) if properties: serialized["properties"] = properties # Update the serialized object with any items to include after properties. # Do not overwrite properties already existing in the serialized dict. if cls.after_items_update: after_items_update = {} for entry in cls.after_items_update: for name, data in entry.items(): if name in after_items_update: if isinstance(after_items_update[name], list): after_items_update[name].extend(data) else: after_items_update[name] = data if after_items_update: after_items_update.update(serialized) serialized = after_items_update if required: # Only include required if not empty serialized["required"] = required if ordering: # Only include ordering if not empty serialized["x-ordering"] = ordering serialized["additionalProperties"] = cls.__allow_additional_items__ return serialized @classmethod def defaults(cls): serialized = cls.serialize() defaults = {} for name, details in serialized["properties"].items(): if "default" in details: defaults[name] = details["default"] continue if "properties" in details: for sname, sdetails in details["properties"].items(): if "default" in sdetails: defaults.setdefault(name, {})[sname] = sdetails["default"] continue return defaults @classmethod def as_requirements_item(cls): serialized_schema = cls.serialize() required = serialized_schema.get("required", []) for name in serialized_schema["properties"]: if name not in required: required.append(name) return RequirementsItem(requirements=required) # @classmethod # def render_as_rst(cls): # ''' # Render the configuration block as a restructured text string # ''' # # TODO: Implement RST rendering # raise NotImplementedError # @classmethod # def render_as_yaml(cls): # ''' # Render the configuration block as a parseable YAML string including comments # ''' # # TODO: Implement YAML rendering # raise NotImplementedError class SchemaItem(metaclass=BaseSchemaItemMeta): """ Base configuration items class. All configurations must subclass it """ # Define some class level attributes to make PyLint happier __type__ = None __format__ = None _attributes = None __flatten__ = False __serialize_attr_aliases__ = None required = False def __init__(self, required=None, **extra): """ :param required: If the configuration item is required. Defaults to ``False``. """ if required is not None: self.required = required self.extra = extra def __validate_attributes__(self): """ Run any validation check you need the instance attributes. ATTENTION: Don't call the parent class when overriding this method because it will just duplicate the executions. This class'es metaclass will take care of that. """ if self.required not in (True, False): raise RuntimeError("'required' can only be True/False") def _get_argname_value(self, argname): """ Return the argname value looking up on all possible attributes """ # Let's see if there's a private function to get the value argvalue = getattr(self, f"__get_{argname}__", None) if argvalue is not None and callable(argvalue): argvalue = argvalue() # pylint: disable=not-callable if argvalue is None: # Let's see if the value is defined as a public class variable argvalue = getattr(self, argname, None) if argvalue is None: # Let's see if it's defined as a private class variable argvalue = getattr(self, f"__{argname}__", None) if argvalue is None: # Let's look for it in the extra dictionary argvalue = self.extra.get(argname, None) return argvalue def serialize(self): """ Return a serializable form of the config instance """ raise NotImplementedError class BaseSchemaItem(SchemaItem): """ Base configuration items class. All configurations must subclass it """ # Let's define description as a class attribute, this will allow a custom configuration # item to do something like: # class MyCustomConfig(StringItem): # ''' # This is my custom config, blah, blah, blah # ''' # description = __doc__ # description = None # The same for all other base arguments title = None default = None enum = None enumNames = None def __init__( self, title=None, description=None, default=None, enum=None, enumNames=None, **kwargs, ): """ :param required: If the configuration item is required. Defaults to ``False``. :param title: A short explanation about the purpose of the data described by this item. :param description: A detailed explanation about the purpose of the data described by this item. :param default: The default value for this configuration item. May be :data:`.Null` (a special value to set the default value to null). :param enum: A list(list, tuple, set) of valid choices. """ if title is not None: self.title = title if description is not None: self.description = description if default is not None: self.default = default if enum is not None: self.enum = enum if enumNames is not None: self.enumNames = enumNames super().__init__(**kwargs) def __validate_attributes__(self): if self.enum is not None: if not isinstance(self.enum, (list, tuple, set)): raise RuntimeError( "Only the 'list', 'tuple' and 'set' python types can be used " "to define 'enum'" ) if not isinstance(self.enum, list): self.enum = list(self.enum) if self.enumNames is not None: if not isinstance(self.enumNames, (list, tuple, set)): raise RuntimeError( "Only the 'list', 'tuple' and 'set' python types can be used " "to define 'enumNames'" ) if len(self.enum) != len(self.enumNames): raise RuntimeError( "The size of 'enumNames' must match the size of 'enum'" ) if not isinstance(self.enumNames, list): self.enumNames = list(self.enumNames) def serialize(self): """ Return a serializable form of the config instance """ serialized = {"type": self.__type__} for argname in self._attributes: if argname == "required": # This is handled elsewhere continue argvalue = self._get_argname_value(argname) if argvalue is not None: if argvalue is Null: argvalue = None # None values are not meant to be included in the # serialization, since this is not None... if ( self.__serialize_attr_aliases__ and argname in self.__serialize_attr_aliases__ ): argname = self.__serialize_attr_aliases__[argname] serialized[argname] = argvalue return serialized def __get_description__(self): if self.description is not None: if self.description == self.__doc__: return textwrap.dedent(self.description).strip() return self.description # def render_as_rst(self, name): # ''' # Render the configuration item as a restructured text string # ''' # # TODO: Implement YAML rendering # raise NotImplementedError # def render_as_yaml(self, name): # ''' # Render the configuration item as a parseable YAML string including comments # ''' # # TODO: Include the item rules in the output, minimum, maximum, etc... # output = '# ----- ' # output += self.title # output += ' ' # output += '-' * (RENDER_COMMENT_YAML_MAX_LINE_LENGTH - 7 - len(self.title) - 2) # output += '>\n' # if self.description: # output += '\n'.join(textwrap.wrap(self.description, # width=RENDER_COMMENT_YAML_MAX_LINE_LENGTH, # initial_indent='# ')) # output += '\n' # yamled_default_value = salt.utils.yaml.safe_dump(self.default, default_flow_style=False).split('\n...', 1)[0] # output += '# Default: {0}\n'.format(yamled_default_value) # output += '#{0}: {1}\n'.format(name, yamled_default_value) # output += '# <---- ' # output += self.title # output += ' ' # output += '-' * (RENDER_COMMENT_YAML_MAX_LINE_LENGTH - 7 - len(self.title) - 1) # return output + '\n' class NullItem(BaseSchemaItem): __type__ = "null" class BooleanItem(BaseSchemaItem): __type__ = "boolean" class StringItem(BaseSchemaItem): """ A string configuration field """ __type__ = "string" __serialize_attr_aliases__ = {"min_length": "minLength", "max_length": "maxLength"} format = None pattern = None min_length = None max_length = None def __init__( self, format=None, # pylint: disable=redefined-builtin pattern=None, min_length=None, max_length=None, **kwargs, ): """ :param required: If the configuration item is required. Defaults to ``False``. :param title: A short explanation about the purpose of the data described by this item. :param description: A detailed explanation about the purpose of the data described by this item. :param default: The default value for this configuration item. May be :data:`.Null` (a special value to set the default value to null). :param enum: A list(list, tuple, set) of valid choices. :param format: A semantic format of the string (for example, ``"date-time"``, ``"email"``, or ``"uri"``). :param pattern: A regular expression (ECMA 262) that a string value must match. :param min_length: The minimum length :param max_length: The maximum length """ if format is not None: # pylint: disable=redefined-builtin self.format = format if pattern is not None: self.pattern = pattern if min_length is not None: self.min_length = min_length if max_length is not None: self.max_length = max_length super().__init__(**kwargs) def __validate_attributes__(self): if self.format is None and self.__format__ is not None: self.format = self.__format__ class EMailItem(StringItem): """ An internet email address, see `RFC 5322, section 3.4.1`__. .. __: http://tools.ietf.org/html/rfc5322 """ __format__ = "email" class IPv4Item(StringItem): """ An IPv4 address configuration field, according to dotted-quad ABNF syntax as defined in `RFC 2673, section 3.2`__. .. __: http://tools.ietf.org/html/rfc2673 """ __format__ = "ipv4" class IPv6Item(StringItem): """ An IPv6 address configuration field, as defined in `RFC 2373, section 2.2`__. .. __: http://tools.ietf.org/html/rfc2373 """ __format__ = "ipv6" class HostnameItem(StringItem): """ An Internet host name configuration field, see `RFC 1034, section 3.1`__. .. __: http://tools.ietf.org/html/rfc1034 """ __format__ = "hostname" class DateTimeItem(StringItem): """ An ISO 8601 formatted date-time configuration field, as defined by `RFC 3339, section 5.6`__. .. __: http://tools.ietf.org/html/rfc3339 """ __format__ = "date-time" class UriItem(StringItem): """ A universal resource identifier (URI) configuration field, according to `RFC3986`__. .. __: http://tools.ietf.org/html/rfc3986 """ __format__ = "uri" class SecretItem(StringItem): """ A string configuration field containing a secret, for example, passwords, API keys, etc """ __format__ = "secret" class NumberItem(BaseSchemaItem): __type__ = "number" __serialize_attr_aliases__ = { "multiple_of": "multipleOf", "exclusive_minimum": "exclusiveMinimum", "exclusive_maximum": "exclusiveMaximum", } multiple_of = None minimum = None exclusive_minimum = None maximum = None exclusive_maximum = None def __init__( self, multiple_of=None, minimum=None, exclusive_minimum=None, maximum=None, exclusive_maximum=None, **kwargs, ): """ :param required: If the configuration item is required. Defaults to ``False``. :param title: A short explanation about the purpose of the data described by this item. :param description: A detailed explanation about the purpose of the data described by this item. :param default: The default value for this configuration item. May be :data:`.Null` (a special value to set the default value to null). :param enum: A list(list, tuple, set) of valid choices. :param multiple_of: A value must be a multiple of this factor. :param minimum: The minimum allowed value :param exclusive_minimum: Whether a value is allowed to be exactly equal to the minimum :param maximum: The maximum allowed value :param exclusive_maximum: Whether a value is allowed to be exactly equal to the maximum """ if multiple_of is not None: self.multiple_of = multiple_of if minimum is not None: self.minimum = minimum if exclusive_minimum is not None: self.exclusive_minimum = exclusive_minimum if maximum is not None: self.maximum = maximum if exclusive_maximum is not None: self.exclusive_maximum = exclusive_maximum super().__init__(**kwargs) class IntegerItem(NumberItem): __type__ = "integer" class ArrayItem(BaseSchemaItem): __type__ = "array" __serialize_attr_aliases__ = { "min_items": "minItems", "max_items": "maxItems", "unique_items": "uniqueItems", "additional_items": "additionalItems", } items = None min_items = None max_items = None unique_items = None additional_items = None def __init__( self, items=None, min_items=None, max_items=None, unique_items=None, additional_items=None, **kwargs, ): """ :param required: If the configuration item is required. Defaults to ``False``. :param title: A short explanation about the purpose of the data described by this item. :param description: A detailed explanation about the purpose of the data described by this item. :param default: The default value for this configuration item. May be :data:`.Null` (a special value to set the default value to null). :param enum: A list(list, tuple, set) of valid choices. :param items: Either of the following: * :class:`BaseSchemaItem` -- all items of the array must match the field schema; * a list or a tuple of :class:`fields <.BaseSchemaItem>` -- all items of the array must be valid according to the field schema at the corresponding index (tuple typing); :param min_items: Minimum length of the array :param max_items: Maximum length of the array :param unique_items: Whether all the values in the array must be distinct. :param additional_items: If the value of ``items`` is a list or a tuple, and the array length is larger than the number of fields in ``items``, then the additional items are described by the :class:`.BaseField` passed using this argument. :type additional_items: bool or :class:`.BaseSchemaItem` """ if items is not None: self.items = items if min_items is not None: self.min_items = min_items if max_items is not None: self.max_items = max_items if unique_items is not None: self.unique_items = unique_items if additional_items is not None: self.additional_items = additional_items super().__init__(**kwargs) def __validate_attributes__(self): if not self.items and not self.additional_items: raise RuntimeError("One of items or additional_items must be passed.") if self.items is not None: if isinstance(self.items, (list, tuple)): for item in self.items: if not isinstance(item, (Schema, SchemaItem)): raise RuntimeError( "All items passed in the item argument tuple/list must be " "a subclass of Schema, SchemaItem or BaseSchemaItem, " "not {}".format(type(item)) ) elif not isinstance(self.items, (Schema, SchemaItem)): raise RuntimeError( "The items argument passed must be a subclass of " "Schema, SchemaItem or BaseSchemaItem, not " "{}".format(type(self.items)) ) def __get_items__(self): if isinstance(self.items, (Schema, SchemaItem)): # This is either a Schema or a Basetem, return it in its # serialized form return self.items.serialize() if isinstance(self.items, (tuple, list)): items = [] for item in self.items: items.append(item.serialize()) return items class DictItem(BaseSchemaItem): __type__ = "object" __serialize_attr_aliases__ = { "min_properties": "minProperties", "max_properties": "maxProperties", "pattern_properties": "patternProperties", "additional_properties": "additionalProperties", } properties = None pattern_properties = None additional_properties = None min_properties = None max_properties = None def __init__( self, properties=None, pattern_properties=None, additional_properties=None, min_properties=None, max_properties=None, **kwargs, ): """ :param required: If the configuration item is required. Defaults to ``False``. :type required: boolean :param title: A short explanation about the purpose of the data described by this item. :type title: str :param description: A detailed explanation about the purpose of the data described by this item. :param default: The default value for this configuration item. May be :data:`.Null` (a special value to set the default value to null). :param enum: A list(list, tuple, set) of valid choices. :param properties: A dictionary containing fields :param pattern_properties: A dictionary whose keys are regular expressions (ECMA 262). Properties match against these regular expressions, and for any that match, the property is described by the corresponding field schema. :type pattern_properties: dict[str -> :class:`.Schema` or :class:`.SchemaItem` or :class:`.BaseSchemaItem`] :param additional_properties: Describes properties that are not described by the ``properties`` or ``pattern_properties``. :type additional_properties: bool or :class:`.Schema` or :class:`.SchemaItem` or :class:`.BaseSchemaItem` :param min_properties: A minimum number of properties. :type min_properties: int :param max_properties: A maximum number of properties :type max_properties: int """ if properties is not None: self.properties = properties if pattern_properties is not None: self.pattern_properties = pattern_properties if additional_properties is not None: self.additional_properties = additional_properties if min_properties is not None: self.min_properties = min_properties if max_properties is not None: self.max_properties = max_properties super().__init__(**kwargs) def __validate_attributes__(self): if ( not self.properties and not self.pattern_properties and not self.additional_properties ): raise RuntimeError( "One of properties, pattern_properties or additional_properties must be" " passed" ) if self.properties is not None: if not isinstance(self.properties, (Schema, dict)): raise RuntimeError( "The passed properties must be passed as a dict or " " a Schema not '{}'".format(type(self.properties)) ) if not isinstance(self.properties, Schema): for key, prop in self.properties.items(): if not isinstance(prop, (Schema, SchemaItem)): raise RuntimeError( "The passed property who's key is '{}' must be of type " "Schema, SchemaItem or BaseSchemaItem, not " "'{}'".format(key, type(prop)) ) if self.pattern_properties is not None: if not isinstance(self.pattern_properties, dict): raise RuntimeError( "The passed pattern_properties must be passed as a dict " "not '{}'".format(type(self.pattern_properties)) ) for key, prop in self.pattern_properties.items(): if not isinstance(prop, (Schema, SchemaItem)): raise RuntimeError( "The passed pattern_property who's key is '{}' must " "be of type Schema, SchemaItem or BaseSchemaItem, " "not '{}'".format(key, type(prop)) ) if self.additional_properties is not None: if not isinstance(self.additional_properties, (bool, Schema, SchemaItem)): raise RuntimeError( "The passed additional_properties must be of type bool, " "Schema, SchemaItem or BaseSchemaItem, not '{}'".format( type(self.pattern_properties) ) ) def __get_properties__(self): if self.properties is None: return if isinstance(self.properties, Schema): return self.properties.serialize()["properties"] properties = OrderedDict() for key, prop in self.properties.items(): properties[key] = prop.serialize() return properties def __get_pattern_properties__(self): if self.pattern_properties is None: return pattern_properties = OrderedDict() for key, prop in self.pattern_properties.items(): pattern_properties[key] = prop.serialize() return pattern_properties def __get_additional_properties__(self): if self.additional_properties is None: return if isinstance(self.additional_properties, bool): return self.additional_properties return self.additional_properties.serialize() def __call__(self, flatten=False): self.__flatten__ = flatten return self def serialize(self): result = super().serialize() required = [] if self.properties is not None: if isinstance(self.properties, Schema): serialized = self.properties.serialize() if "required" in serialized: required.extend(serialized["required"]) else: for key, prop in self.properties.items(): if prop.required: required.append(key) if required: result["required"] = required return result class RequirementsItem(SchemaItem): __type__ = "object" requirements = None def __init__(self, requirements=None): if requirements is not None: self.requirements = requirements super().__init__() def __validate_attributes__(self): if self.requirements is None: raise RuntimeError("The passed requirements must not be empty") if not isinstance(self.requirements, (SchemaItem, list, tuple, set)): raise RuntimeError( "The passed requirements must be passed as a list, tuple, " "set SchemaItem or BaseSchemaItem, not '{}'".format(self.requirements) ) if not isinstance(self.requirements, SchemaItem): if not isinstance(self.requirements, list): self.requirements = list(self.requirements) for idx, item in enumerate(self.requirements): if not isinstance(item, ((str,), SchemaItem)): raise RuntimeError( "The passed requirement at the {} index must be of type " "str or SchemaItem, not '{}'".format(idx, type(item)) ) def serialize(self): if isinstance(self.requirements, SchemaItem): requirements = self.requirements.serialize() else: requirements = [] for requirement in self.requirements: if isinstance(requirement, SchemaItem): requirements.append(requirement.serialize()) continue requirements.append(requirement) return {"required": requirements} class OneOfItem(SchemaItem): __type__ = "oneOf" items = None def __init__(self, items=None, required=None): if items is not None: self.items = items super().__init__(required=required) def __validate_attributes__(self): if not self.items: raise RuntimeError("The passed items must not be empty") if not isinstance(self.items, (list, tuple)): raise RuntimeError( "The passed items must be passed as a list/tuple not '{}'".format( type(self.items) ) ) for idx, item in enumerate(self.items): if not isinstance(item, (Schema, SchemaItem)): raise RuntimeError( "The passed item at the {} index must be of type " "Schema, SchemaItem or BaseSchemaItem, not " "'{}'".format(idx, type(item)) ) if not isinstance(self.items, list): self.items = list(self.items) def __call__(self, flatten=False): self.__flatten__ = flatten return self def serialize(self): return {self.__type__: [i.serialize() for i in self.items]} class AnyOfItem(OneOfItem): __type__ = "anyOf" class AllOfItem(OneOfItem): __type__ = "allOf" class NotItem(SchemaItem): __type__ = "not" item = None def __init__(self, item=None): if item is not None: self.item = item super().__init__() def __validate_attributes__(self): if not self.item: raise RuntimeError("An item must be passed") if not isinstance(self.item, (Schema, SchemaItem)): raise RuntimeError( "The passed item be of type Schema, SchemaItem or " "BaseSchemaItem, not '{}'".format(type(self.item)) ) def serialize(self): return {self.__type__: self.item.serialize()} # ----- Custom Preconfigured Configs --------------------------------------------------------------------------------> class PortItem(IntegerItem): minimum = 0 # yes, 0 is a valid port number maximum = 65535 # <---- Custom Preconfigured Configs --------------------------------------------------------------------------------- class ComplexSchemaItem(BaseSchemaItem): """ .. versionadded:: 2016.11.0 Complex Schema Item """ # This attribute is populated by the metaclass, but pylint fails to see it # and assumes it's not an iterable _attributes = [] _definition_name = None def __init__(self, definition_name=None, required=None): super().__init__(required=required) self.__type__ = "object" self._definition_name = ( definition_name if definition_name else self.__class__.__name__ ) # Schema attributes might have been added as class attributes so we # and they must be added to the _attributes attr self._add_missing_schema_attributes() def _add_missing_schema_attributes(self): """ Adds any missed schema attributes to the _attributes list The attributes can be class attributes and they won't be included in the _attributes list automatically """ for attr in [attr for attr in dir(self) if not attr.startswith("__")]: attr_val = getattr(self, attr) if ( isinstance(getattr(self, attr), SchemaItem) and attr not in self._attributes ): self._attributes.append(attr) @property def definition_name(self): return self._definition_name def serialize(self): """ The serialization of the complex item is a pointer to the item definition """ return {"$ref": f"#/definitions/{self.definition_name}"} def get_definition(self): """Returns the definition of the complex item""" serialized = super().serialize() # Adjust entries in the serialization del serialized["definition_name"] serialized["title"] = self.definition_name properties = {} required_attr_names = [] for attr_name in self._attributes: attr = getattr(self, attr_name) if attr and isinstance(attr, BaseSchemaItem): # Remove the attribute entry added by the base serialization del serialized[attr_name] properties[attr_name] = attr.serialize() properties[attr_name]["type"] = attr.__type__ if attr.required: required_attr_names.append(attr_name) if serialized.get("properties") is None: serialized["properties"] = {} serialized["properties"].update(properties) # Assign the required array if required_attr_names: serialized["required"] = required_attr_names return serialized def get_complex_attrs(self): """Returns a dictionary of the complex attributes""" return [ getattr(self, attr_name) for attr_name in self._attributes if isinstance(getattr(self, attr_name), ComplexSchemaItem) ] class DefinitionsSchema(Schema): """ .. versionadded:: 2016.11.0 JSON schema class that supports ComplexSchemaItem objects by adding a definitions section to the JSON schema, containing the item definitions. All references to ComplexSchemaItems are built using schema inline dereferencing. """ @classmethod def serialize(cls, id_=None): # Get the initial serialization serialized = super().serialize(id_) complex_items = [] # Augment the serializations with the definitions of all complex items aux_items = cls._items.values() # Convert dict_view object to a list on Python 3 aux_items = list(aux_items) while aux_items: item = aux_items.pop(0) # Add complex attributes if isinstance(item, ComplexSchemaItem): complex_items.append(item) aux_items.extend(item.get_complex_attrs()) # Handle container items if isinstance(item, OneOfItem): aux_items.extend(item.items) elif isinstance(item, ArrayItem): aux_items.append(item.items) elif isinstance(item, DictItem): if item.properties: aux_items.extend(item.properties.values()) if item.additional_properties and isinstance( item.additional_properties, SchemaItem ): aux_items.append(item.additional_properties) definitions = OrderedDict() for config in complex_items: if isinstance(config, ComplexSchemaItem): definitions[config.definition_name] = config.get_definition() serialized["definitions"] = definitions return serialized