zeroSteiner / rule-engine

A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
https://zerosteiner.github.io/rule-engine/
BSD 3-Clause "New" or "Revised" License
433 stars 54 forks source link

how do I coerce string to datetime? #63

Closed xarses closed 1 year ago

xarses commented 1 year ago

I have data structures that may contain strings that are serialized dates.

Simplified I'm trying to read

{
    'config':[
        {'created': '2023-06-27T17:27:22.803255939-07:00'}
    ]
}

I have a rule like "[ cfg for cfg in config if cfg.created > $today ]" I'm trying to have it explicitly cast to a datetime so I can evaluate the date, but no matter what I try, I can't find a flexible way to resolve it and keep a flexible description of the given object

When parsing I get

>>> r = Rule("[ cfg for cfg in config if cfg.created > $today ]",)
>>> r.matches(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 636, in matches
    return bool(self.evaluate(thing))
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 625, in evaluate
    return self.statement.evaluate(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 1056, in evaluate
    return self.expression.evaluate(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 693, in evaluate
    if self.condition is None or self.condition.evaluate(thing):
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 386, in evaluate
    return self._evaluator(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 591, in __op_arithmetic
    return self.__op_arithmetic_values(op, left_value, right_value)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 607, in __op_arithmetic_values
    raise errors.EvaluationError('data type mismatch')
rule_engine.errors.EvaluationError: data type mismatch

If I change this to what appears to be the way to coerce it to a date, it still freaks out (both cfg.created.date and (cfg.created).date)

>>> r = Rule("[ cfg for cfg in config if (cfg.created).date > $today ]",)
>>> r.matches(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 636, in matches
    return bool(self.evaluate(thing))
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 625, in evaluate
    return self.statement.evaluate(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 1056, in evaluate
    return self.expression.evaluate(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 693, in evaluate
    if self.condition is None or self.condition.evaluate(thing):
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 386, in evaluate
    return self._evaluator(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 589, in __op_arithmetic
    left_value = self.left.evaluate(thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 806, in evaluate
    raise attribute_error from None
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 790, in evaluate
    value = self.context.resolve_attribute(thing, resolved_obj, self.name)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 536, in resolve_attribute
    return self.__resolve_attribute(thing, object_, name)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 140, in __call__
    resolver = self._get_resolver(object_type, name, thing=thing)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 158, in _get_resolver
    raise errors.AttributeResolutionError(name, object_type, thing=thing, suggestion=suggest_symbol(name, attribute_resolvers.keys()))
rule_engine.errors.AttributeResolutionError: ('date', <_DataTypeDef name=STRING python_type=str >)

If I load a context and explicitly declare the date it can resolve the value, but It doesn't look like there is a way to do this with out declaring the type for every possible object we want to access.

ctx = rule_engine.Context(
    resolver=rule_engine.resolve_attribute,
    type_resolver = rule_engine.type_resolver_from_dict({
        'created': rule_engine.DataType.DATETIME,
    })
)

>>> r = Rule("[ cfg for cfg in config if cfg.created > $today ]", context = ctx)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 578, in __init__
    self.statement = self.parser.parse(text, context)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/parser.py", line 106, in parse
    return result.build()
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/parser.py", line 60, in build
    return constructor(*self.args, **self.kwargs)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 1053, in build
    return cls(context, expression.build(), **kwargs).reduce()
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/parser.py", line 60, in build
    return constructor(*self.args, **self.kwargs)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 672, in build
    iterable = iterable.build()
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/parser.py", line 60, in build
    return constructor(*self.args, **self.kwargs)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 102, in build
    return cls(*args, **kwargs).reduce()
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/ast.py", line 991, in __init__
    type_hint = context.resolve_type(name, scope=scope)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 555, in resolve_type
    return self.__type_resolver(name)
  File "/Users/andrew.woodward/.pyenv/versions/3.8.16/lib/python3.8/site-packages/rule_engine/engine.py", line 96, in _type_resolver
    raise errors.SymbolResolutionError(name, suggestion=suggest_symbol(name, type_map.keys()))
rule_engine.errors.SymbolResolutionError: config
>>> 
zeroSteiner commented 1 year ago

Right now you can't coerce a string into a datetime value. You'd need to parse it in Python yourself and pass it in as a datetime.datetime instance instead of a string.

Now having said that, I've started working on #32 and I'll add $parse_datetime to my list of initial functions. Once that's done you'll be able to do something like $parse_datetime(cfg.created) > $today and it'll work. There is currently no ETA on when that'll be done.

Until then though, you'll have to parse the string yourself and pass the datetime.datetime instance in the object you're evaluating.