Source code for rule_engine.builtins

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  rule_engine/builtins.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import collections
import collections.abc
import datetime
import decimal
import functools
import math
import random

from . import ast
from . import errors
from . import types
from .parser.utilities import parse_datetime, parse_float, parse_timedelta

import dateutil.tz

def _builtin_filter(function, iterable):
	return tuple(filter(function, iterable))

def _builtin_map(function, iterable):
	return tuple(map(function, iterable))

def _builtin_parse_datetime(builtins, string):
	return parse_datetime(string, builtins.timezone)

def _builtin_random(boundary=None):
	if boundary:
		if not types.is_natural_number(boundary):
			raise errors.FunctionCallError('argument #1 (boundary) must be a natural number')
		return random.randint(0, int(boundary))
	return random.random()

def _builtin_range(start, stop=None, step=None):
	if not types.is_integer_number(start):
		raise errors.FunctionCallError('argument #1 (start) must be an integer number')
	if stop:
		if not types.is_integer_number(stop):
			raise errors.FunctionCallError('argument #2 (stop) must be an integer number')
		if step:
			if not types.is_integer_number(step):
				raise errors.FunctionCallError('argument #3 (step) must be an integer number')
			return list(range(int(start), int(stop), int(step)))
		return list(range(int(start), int(stop)))
	return list(range(int(start)))

def _builtins_split(string, sep=None, maxsplit=None):
	if maxsplit is None:
		maxsplit = -1
	elif types.is_natural_number(maxsplit):
		maxsplit = int(maxsplit)
	else:
		raise errors.FunctionCallError('argument #3 (maxsplit) must be a natural number')
	return tuple(string.split(sep=sep, maxsplit=maxsplit))

class BuiltinValueGenerator(object):
	"""
	A class used as a wrapper for builtin values to differentiate between a value that is a function and a value that
	should be generated by calling a function. A value that is generated by calling a function is useful for determining
	the value during evaluation for things like the current time.

	.. versionadded:: 4.0.0
	"""
	__slots__ = ('callable',)
	def __init__(self, callable):
		self.callable = callable

	def __call__(self, builtins):
		return self.callable(builtins)

[docs] class Builtins(collections.abc.Mapping): """ A class to define and provide variables to within the builtin context of rules. These can be accessed by specifying a symbol name with the ``$`` prefix. """ scope_name = 'built-in' """The identity name of the scope for builtin symbols."""
[docs] def __init__(self, values, namespace=None, timezone=None, value_types=None): """ :param dict values: A mapping of string keys to be used as symbol names with values of either Python literals or a function which will be called when the symbol is accessed. When using a function, it will be passed a single argument, which is the instance of :py:class:`Builtins`. :param str namespace: The namespace of the variables to resolve. :param timezone: A timezone to use when resolving timestamps. :type timezone: :py:class:`~datetime.tzinfo` :param dict value_types: A mapping of the values to their datatypes. .. versionchanged:: 2.3.0 Added the *value_types* parameter. """ self.__values = values self.__value_types = value_types or {} self.namespace = namespace self.timezone = timezone or dateutil.tz.tzlocal()
[docs] def resolve_type(self, name): """ The method to use for resolving the data type of a builtin symbol. :param str name: The name of the symbol to retrieve the data type of. :return: The data type of the symbol or :py:attr:`~rule_engine.ast.DataType.UNDEFINED`. """ return self.__value_types.get(name, ast.DataType.UNDEFINED)
def __repr__(self): return "<{} namespace={!r} keys={!r} timezone={!r} >".format(self.__class__.__name__, self.namespace, tuple(self.keys()), self.timezone) def __getitem__(self, name): value = self.__values[name] if isinstance(value, collections.abc.Mapping): if self.namespace is None: namespace = name else: namespace = self.namespace + '.' + name return self.__class__(value, namespace=namespace, timezone=self.timezone) elif callable(value) and isinstance(value, BuiltinValueGenerator): value = value(self) return value def __iter__(self): return iter(self.__values) def __len__(self): return len(self.__values)
[docs] @classmethod def from_defaults(cls, values=None, **kwargs): """Initialize a :py:class:`Builtins` instance with a set of default values.""" now = BuiltinValueGenerator(lambda builtins: datetime.datetime.now(tz=builtins.timezone)) # there may be errors here if the decimal.Context precision exceeds what is provided by the math constants default_values = { # mathematical constants 'e': decimal.Decimal(repr(math.e)), 'pi': decimal.Decimal(repr(math.pi)), # timestamps 'now': now, 'today': BuiltinValueGenerator(lambda builtins: now(builtins).replace(hour=0, minute=0, second=0, microsecond=0)), # functions 'abs': abs, 'any': any, 'all': all, 'sum': sum, 'map': _builtin_map, 'max': max, 'min': min, 'filter': _builtin_filter, 'parse_datetime': BuiltinValueGenerator(lambda builtins: functools.partial(_builtin_parse_datetime, builtins)), 'parse_float': parse_float, 'parse_timedelta': parse_timedelta, 'random': _builtin_random, 'range': _builtin_range, 'split': _builtins_split } default_values.update(values or {}) default_value_types = { # mathematical constants 'e': ast.DataType.FLOAT, 'pi': ast.DataType.FLOAT, # timestamps 'now': ast.DataType.DATETIME, 'today': ast.DataType.DATETIME, # functions 'abs': ast.DataType.FUNCTION('abs', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.FLOAT,)), 'all': ast.DataType.FUNCTION('all', return_type=ast.DataType.BOOLEAN, argument_types=(ast.DataType.ARRAY,)), 'any': ast.DataType.FUNCTION('any', return_type=ast.DataType.BOOLEAN, argument_types=(ast.DataType.ARRAY,)), 'sum': ast.DataType.FUNCTION('sum', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)), 'map': ast.DataType.FUNCTION('map', return_type=ast.DataType.ARRAY, argument_types=(ast.DataType.FUNCTION, ast.DataType.ARRAY)), 'max': ast.DataType.FUNCTION('max', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)), 'min': ast.DataType.FUNCTION('min', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)), 'filter': ast.DataType.FUNCTION('filter', return_type=ast.DataType.ARRAY, argument_types=(ast.DataType.FUNCTION, ast.DataType.ARRAY)), 'parse_datetime': ast.DataType.FUNCTION('parse_datetime', return_type=ast.DataType.DATETIME, argument_types=(ast.DataType.STRING,)), 'parse_float': ast.DataType.FUNCTION('parse_float', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.STRING,)), 'parse_timedelta': ast.DataType.FUNCTION('parse_timedelta', return_type=ast.DataType.TIMEDELTA, argument_types=(ast.DataType.STRING,)), 'random': ast.DataType.FUNCTION('random', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.FLOAT,), minimum_arguments=0), 'range': ast.DataType.FUNCTION('range', return_type=ast.DataType.ARRAY(ast.DataType.FLOAT), argument_types=(ast.DataType.FLOAT, ast.DataType.FLOAT, ast.DataType.FLOAT,), minimum_arguments=1), 'split': ast.DataType.FUNCTION( 'split', return_type=ast.DataType.ARRAY(ast.DataType.STRING), argument_types=(ast.DataType.STRING, ast.DataType.STRING, ast.DataType.FLOAT), minimum_arguments=1 ) } default_value_types.update(kwargs.pop('value_types', {})) return cls(default_values, value_types=default_value_types, **kwargs)