rule-engine 5.0.2__tar.gz → 5.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {rule_engine-5.0.2 → rule_engine-5.1.0}/PKG-INFO +2 -1
  2. {rule_engine-5.0.2 → rule_engine-5.1.0}/README.rst +1 -0
  3. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/__init__.py +5 -1
  4. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/engine/__init__.py +1 -0
  5. rule_engine-5.1.0/lib/rule_engine/engine/chain.py +289 -0
  6. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/engine/context.py +3 -0
  7. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/errors.py +24 -0
  8. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine.egg-info/PKG-INFO +2 -1
  9. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine.egg-info/SOURCES.txt +1 -0
  10. {rule_engine-5.0.2 → rule_engine-5.1.0}/LICENSE +0 -0
  11. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/_vendor/__init__.py +0 -0
  12. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/_vendor/ply/LICENSE +0 -0
  13. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/_vendor/ply/__init__.py +0 -0
  14. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/_vendor/ply/lex.py +0 -0
  15. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/_vendor/ply/yacc.py +0 -0
  16. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/__init__.py +0 -0
  17. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/base.py +0 -0
  18. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/binary/__init__.py +0 -0
  19. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/binary/arithmetic.py +0 -0
  20. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/binary/base.py +0 -0
  21. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/binary/comparison.py +0 -0
  22. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/expression/__init__.py +0 -0
  23. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/expression/access.py +0 -0
  24. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/expression/control.py +0 -0
  25. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/expression/resolution.py +0 -0
  26. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/ast/literal.py +0 -0
  27. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/builtins.py +0 -0
  28. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/debug_ast.py +0 -0
  29. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/debug_repl.py +0 -0
  30. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/engine/_attribute_resolver.py +0 -0
  31. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/engine/rule.py +0 -0
  32. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/parser/__init__.py +0 -0
  33. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/parser/base.py +0 -0
  34. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/parser/utilities.py +0 -0
  35. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/suggestions.py +0 -0
  36. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/__init__.py +0 -0
  37. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/_object.py +0 -0
  38. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/coercion.py +0 -0
  39. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/dataclass.py +0 -0
  40. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/datatype.py +0 -0
  41. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/definitions.py +0 -0
  42. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine/types/sqlalchemy.py +0 -0
  43. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine.egg-info/dependency_links.txt +0 -0
  44. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine.egg-info/requires.txt +0 -0
  45. {rule_engine-5.0.2 → rule_engine-5.1.0}/lib/rule_engine.egg-info/top_level.txt +0 -0
  46. {rule_engine-5.0.2 → rule_engine-5.1.0}/pyproject.toml +0 -0
  47. {rule_engine-5.0.2 → rule_engine-5.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rule-engine
3
- Version: 5.0.2
3
+ Version: 5.1.0
4
4
  Summary: A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
5
5
  Author-email: Spencer McIntyre <zeroSteiner@gmail.com>
6
6
  Maintainer-email: Spencer McIntyre <zeroSteiner@gmail.com>
@@ -45,6 +45,7 @@ Python with some inspiration from Ruby. Some features of this language includes:
45
45
  - Compound datatypes (equivalents for Python dict, list and set types)
46
46
  - Data attributes
47
47
  - Thread safety
48
+ - Rule chains that return the value of the first matching rule
48
49
 
49
50
  Example Usage
50
51
  -------------
@@ -22,6 +22,7 @@ Python with some inspiration from Ruby. Some features of this language includes:
22
22
  - Compound datatypes (equivalents for Python dict, list and set types)
23
23
  - Data attributes
24
24
  - Thread safety
25
+ - Rule chains that return the value of the first matching rule
25
26
 
26
27
  Example Usage
27
28
  -------------
@@ -30,17 +30,19 @@
30
30
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
31
  #
32
32
 
33
- __version__ = '5.0.2'
33
+ __version__ = '5.1.0'
34
34
 
35
35
  from .engine import resolve_attribute
36
36
  from .engine import resolve_item
37
37
  from .engine import type_resolver_from_dataclass
38
38
  from .engine import type_resolver_from_dict
39
39
  from .engine import type_resolver_from_sqlalchemy
40
+ from .engine import Chain
40
41
  from .engine import Context
41
42
  from .engine import Rule
42
43
 
43
44
  from .errors import AttributeResolutionError
45
+ from .errors import ChainExhaustedError
44
46
  from .errors import EngineError
45
47
  from .errors import EvaluationError
46
48
  from .errors import RuleSyntaxError
@@ -50,6 +52,8 @@ from .types import DataType
50
52
 
51
53
  __all__ = (
52
54
  'AttributeResolutionError',
55
+ 'Chain',
56
+ 'ChainExhaustedError',
53
57
  'Context',
54
58
  'DataType',
55
59
  'EngineError',
@@ -32,3 +32,4 @@
32
32
 
33
33
  from .context import resolve_attribute, resolve_item, type_resolver_from_dataclass, type_resolver_from_dict, type_resolver_from_sqlalchemy, Context
34
34
  from .rule import Rule, DebugRule
35
+ from .chain import Chain, ChainEntry
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # rule_engine/engine/chain.py
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are
8
+ # met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above
13
+ # copyright notice, this list of conditions and the following disclaimer
14
+ # in the documentation and/or other materials provided with the
15
+ # distribution.
16
+ # * Neither the name of the project nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ import collections.abc
34
+ import operator
35
+ import sys
36
+ import threading
37
+ from typing import Any, Iterable, Iterator, NamedTuple, TypeVar, overload
38
+
39
+ from .. import errors
40
+ from .context import Context
41
+ from .rule import Rule
42
+
43
+ class ChainEntry(NamedTuple):
44
+ """
45
+ A single entry within a :py:class:`.Chain`, pairing a rule with the value that is returned when it matches.
46
+
47
+ .. versionadded:: 5.1.0
48
+ """
49
+ rule: Rule
50
+ """The :py:class:`.Rule` that objects are evaluated against."""
51
+ value: Any
52
+ """The value that is returned by :py:meth:`.Chain.evaluate` when :py:attr:`.rule` matches."""
53
+
54
+ def _check_rule(rule: Any, name: str = 'chain rule') -> None:
55
+ if not isinstance(rule, (str, Rule)):
56
+ raise TypeError("{0} must be a str or Rule instance, not {1}".format(name, type(rule).__name__))
57
+
58
+ def _unpack_entry(position: int, entry: Any) -> tuple[Any, Any]:
59
+ # only accept real sequences, iterating a str or mapping would silently split it into a bogus (rule, value) pair
60
+ if not isinstance(entry, collections.abc.Sequence) or isinstance(entry, (str, bytes, bytearray)):
61
+ raise TypeError("chain entry #{0} must be a (rule, value) pair, not {1}".format(
62
+ position,
63
+ type(entry).__name__
64
+ ))
65
+ if len(entry) != 2:
66
+ raise ValueError("chain entry #{0} has length {1}; 2 is required".format(position, len(entry)))
67
+ _check_rule(entry[0], "chain entry #{0} rule".format(position))
68
+ return entry[0], entry[1]
69
+
70
+ _ChainT = TypeVar('_ChainT', bound='Chain')
71
+
72
+ class Chain(object):
73
+ """
74
+ An ordered collection of rules, each of which is paired with a value. An object is evaluated against each rule in
75
+ order and the value of the first rule that matches it is returned, much like a packet traversing a chain in iptables
76
+ where the value is the target (e.g. ``ACCEPT`` or ``DROP``) and the default value is the chain's policy.
77
+
78
+ A rule matches when :py:meth:`.Rule.matches` returns a truthy value. Exceptions raised while evaluating a rule, such
79
+ as a :py:exc:`~rule_engine.errors.SymbolResolutionError`, are not caught and stop the evaluation.
80
+
81
+ Evaluating a chain does not acquire a lock. Each evaluation uses the entries that were present when it started, so
82
+ one thread can modify a chain while others are evaluating it. Modifications are serialized so concurrent changes
83
+ are not lost, however adding or removing an entry copies the others, so a large chain is best built by passing its
84
+ entries to the constructor. A chain should not be modified from a ``__del__`` method or a :py:mod:`weakref`
85
+ finalizer because the garbage collector can run them while a modification is in progress, which would deadlock.
86
+ A chain can be pickled as long as its context, rules and values can be.
87
+
88
+ Indexes start at 0, unlike the rule numbers used by iptables, and negative indexes count from the end of the chain.
89
+ Slicing a chain returns a new chain with the entries in the slice and the same :py:attr:`.default_value` and
90
+ :py:attr:`.context`. Adding or removing entries in either chain afterwards does not change the other.
91
+
92
+ .. versionadded:: 5.1.0
93
+ """
94
+ def __init__(
95
+ self,
96
+ entries: Iterable[tuple[Rule | str, Any]] | None = None,
97
+ *,
98
+ default_value: Any = errors.UNDEFINED,
99
+ context: Context | None = None
100
+ ) -> None:
101
+ """
102
+ :param entries: An optional iterable of ``(rule, value)`` pairs to initialize the chain with, in order. Each
103
+ rule can be either a :py:class:`.Rule` instance or the text of one, which will be compiled using
104
+ *context*.
105
+ :param default_value: The value that :py:meth:`.evaluate` returns when no rule matches. If this is
106
+ :py:data:`~rule_engine.errors.UNDEFINED` (the default), a
107
+ :py:exc:`~rule_engine.errors.ChainExhaustedError` is raised instead.
108
+ :param context: The context to use for compiling rules that are specified as text. If not specified, a new
109
+ default :py:class:`.Context` is created and shared by all of them. :py:class:`.Rule` instances are used
110
+ as-is and keep their own context.
111
+ :type context: :py:class:`.Context`
112
+ """
113
+ if context is None:
114
+ context = Context()
115
+ self._lock = threading.Lock()
116
+ self.context = context
117
+ """The context that is used for compiling rules that are specified as text."""
118
+ self.default_value = default_value
119
+ """The *default_value* parameter from :py:meth:`~__init__`."""
120
+ self._entries: tuple[ChainEntry, ...] = ()
121
+ if entries is not None:
122
+ # validate every entry before compiling any of them so an invalid one doesn't add symbols to the context
123
+ pairs = [_unpack_entry(position, entry) for position, entry in enumerate(entries)]
124
+ self._entries = tuple(self._new_entry(rule, value) for rule, value in pairs)
125
+
126
+ @overload
127
+ def __getitem__(self, index: int) -> ChainEntry: ...
128
+
129
+ @overload
130
+ def __getitem__(self: _ChainT, index: slice) -> _ChainT: ...
131
+
132
+ def __getitem__(self, index: int | slice) -> 'ChainEntry | Chain':
133
+ if isinstance(index, slice):
134
+ chain = self.__class__(default_value=self.default_value, context=self.context)
135
+ # the entries are immutable so the new chain can share them
136
+ chain._entries = self._entries[index]
137
+ return chain
138
+ index = operator.index(index)
139
+ try:
140
+ return self._entries[index]
141
+ except IndexError:
142
+ raise IndexError('chain index out of range') from None
143
+
144
+ def __getstate__(self) -> dict[str, Any]:
145
+ return {'context': self.context, 'default_value': self.default_value, 'entries': self._entries}
146
+
147
+ def __iter__(self) -> Iterator[ChainEntry]:
148
+ # return the tuple's iterator directly so the entries are captured when iter() is called
149
+ return iter(self._entries)
150
+
151
+ def __len__(self) -> int:
152
+ return len(self._entries)
153
+
154
+ def __repr__(self) -> str:
155
+ return "<{0} entries={1} default_value={2!r} >".format(
156
+ self.__class__.__name__,
157
+ len(self._entries),
158
+ self.default_value
159
+ )
160
+
161
+ def __setstate__(self, state: dict[str, Any]) -> None:
162
+ self.context = state['context']
163
+ self.default_value = state['default_value']
164
+ self._entries = state['entries']
165
+ # recreate the lock which can not be pickled
166
+ self._lock = threading.Lock()
167
+
168
+ def _new_entry(self, rule: Rule | str, value: Any) -> ChainEntry:
169
+ _check_rule(rule)
170
+ if isinstance(rule, str):
171
+ rule = Rule(rule, context=self.context)
172
+ return ChainEntry(rule, value)
173
+
174
+ def append(self, rule: Rule | str, value: Any) -> ChainEntry:
175
+ """
176
+ Add a new entry to the end of the chain, like ``iptables --append``.
177
+
178
+ :param rule: The rule for the new entry, either a :py:class:`.Rule` instance or the text of one, which will be
179
+ compiled using :py:attr:`.context`.
180
+ :param value: The value to return when *rule* matches.
181
+ :return: The new entry.
182
+ :rtype: :py:class:`.ChainEntry`
183
+ """
184
+ entry = self._new_entry(rule, value)
185
+ with self._lock:
186
+ self._entries = self._entries + (entry,)
187
+ return entry
188
+
189
+ def delete(self, index: int) -> ChainEntry:
190
+ """
191
+ Remove the entry at *index* from the chain, like ``iptables --delete``. Indexes start at 0 and negative indexes
192
+ count from the end of the chain.
193
+
194
+ :param int index: The index of the entry to remove.
195
+ :return: The entry that was removed.
196
+ :rtype: :py:class:`.ChainEntry`
197
+ :raises IndexError: There is no entry at *index*.
198
+ """
199
+ index = operator.index(index)
200
+ with self._lock:
201
+ entries = list(self._entries)
202
+ try:
203
+ entry = entries.pop(index)
204
+ except (IndexError, OverflowError):
205
+ raise IndexError('chain index out of range') from None
206
+ self._entries = tuple(entries)
207
+ return entry
208
+
209
+ def evaluate(self, thing: Any) -> Any:
210
+ """
211
+ Evaluate *thing* against each rule in the chain and return the value of the first one that matches. If no rule
212
+ matches, the :py:attr:`.default_value` is returned.
213
+
214
+ :param thing: The object on which to apply the rules.
215
+ :return: The value of the first matching rule, or the default value.
216
+ :raises ChainExhaustedError: No rule matched and :py:attr:`.default_value` is
217
+ :py:data:`~rule_engine.errors.UNDEFINED`.
218
+ """
219
+ default_value = self.default_value
220
+ entry = self.find(thing)
221
+ if entry is not None:
222
+ return entry.value
223
+ if default_value is errors.UNDEFINED:
224
+ raise errors.ChainExhaustedError(thing=thing)
225
+ return default_value
226
+
227
+ def find(self, thing: Any) -> ChainEntry | None:
228
+ """
229
+ Evaluate *thing* against each rule in the chain and return the first entry that matches. Unlike
230
+ :py:meth:`.evaluate`, the default value is not used. This can be used to determine which rule matched, and
231
+ :py:meth:`.index` can then be used to determine its position.
232
+
233
+ :param thing: The object on which to apply the rules.
234
+ :return: The first matching :py:class:`.ChainEntry`, or ``None`` if no rule matched.
235
+ """
236
+ for entry in self._entries:
237
+ if entry.rule.matches(thing):
238
+ return entry
239
+ return None
240
+
241
+ def flush(self) -> None:
242
+ """
243
+ Remove all entries from the chain, like ``iptables --flush``. The :py:attr:`.default_value` is unchanged.
244
+ """
245
+ with self._lock:
246
+ entries, self._entries = self._entries, ()
247
+ # release the old entries after the lock so any finalizers they trigger can modify the chain
248
+ del entries
249
+
250
+ def index(self, entry: ChainEntry) -> int:
251
+ """
252
+ Return the index of *entry* in the chain, such as an entry that was returned by :py:meth:`.find`. Entries are
253
+ compared as tuples and a rule is only equal to itself, so an entry is only found in a chain that contains the
254
+ same rule, like the chain it came from or a slice of it.
255
+
256
+ :param entry: The entry to look up.
257
+ :type entry: :py:class:`.ChainEntry`
258
+ :return: The index of the first entry that is equal to *entry*.
259
+ :rtype: int
260
+ :raises ValueError: *entry* is not in the chain.
261
+ """
262
+ # compare the entries here instead of using tuple.index so an error raised while comparing two values isn't
263
+ # mistaken for the entry being missing
264
+ for position, other in enumerate(self._entries):
265
+ if other is entry or other == entry:
266
+ return position
267
+ raise ValueError('entry is not in the chain')
268
+
269
+ def insert(self, index: int, rule: Rule | str, value: Any) -> ChainEntry:
270
+ """
271
+ Insert a new entry into the chain before *index*, like ``iptables --insert``. Indexes start at 0 and negative
272
+ indexes count from the end of the chain. This follows the semantics of :py:meth:`list.insert`, so an *index*
273
+ past either end of the chain adds the entry to that end.
274
+
275
+ :param int index: The index to insert the new entry before.
276
+ :param rule: The rule for the new entry, either a :py:class:`.Rule` instance or the text of one, which will be
277
+ compiled using :py:attr:`.context`.
278
+ :param value: The value to return when *rule* matches.
279
+ :return: The new entry.
280
+ :rtype: :py:class:`.ChainEntry`
281
+ """
282
+ # clamp the index to the range list.insert supports, it then clamps it again to the ends of the chain
283
+ index = max(-sys.maxsize - 1, min(operator.index(index), sys.maxsize))
284
+ entry = self._new_entry(rule, value)
285
+ with self._lock:
286
+ entries = list(self._entries)
287
+ entries.insert(index, entry)
288
+ self._entries = tuple(entries)
289
+ return entry
@@ -326,6 +326,9 @@ class Context(object):
326
326
  self.symbols = state['symbols']
327
327
  self.default_timezone = state['default_timezone']
328
328
  self.default_value = state['default_value']
329
+ if isinstance(self.default_value, errors._UNDEFINED):
330
+ # pickles created before UNDEFINED preserved its identity restore a copy of it instead of the singleton
331
+ self.default_value = errors.UNDEFINED
329
332
  self.decimal_context = state['decimal_context']
330
333
  self.mapping_attribute_lookup = state['mapping_attribute_lookup']
331
334
  self._mapping_fallback_warned = state['_mapping_fallback_warned']
@@ -46,11 +46,17 @@ class _UNDEFINED(object):
46
46
  __nonzero__ = __bool__
47
47
  def __repr__(self) -> str:
48
48
  return self.__name__
49
+ def __reduce__(self) -> str:
50
+ # restore the module-level singleton so identity checks (``is UNDEFINED``) still work after pickle or copy
51
+ return 'UNDEFINED'
49
52
  UNDEFINED = _UNDEFINED()
50
53
  """
51
54
  A sentinel value to specify that something is undefined. When evaluated, the value is falsy.
52
55
 
53
56
  .. versionadded:: 2.0.0
57
+
58
+ .. versionchanged:: 5.1.0
59
+ The sentinel keeps its identity when it is pickled or copied.
54
60
  """
55
61
 
56
62
  class EngineError(Exception):
@@ -357,3 +363,21 @@ class ArithmeticError(EvaluationError):
357
363
 
358
364
  .. versionadded:: 5.0.0
359
365
  """
366
+
367
+ class ChainExhaustedError(EvaluationError):
368
+ """
369
+ An error raised by :py:meth:`~rule_engine.engine.Chain.evaluate` when none of the rules in the chain match and the
370
+ chain does not have a default value.
371
+
372
+ .. versionadded:: 5.1.0
373
+ """
374
+ def __init__(self, thing: Any = UNDEFINED) -> None:
375
+ """
376
+ :param thing: The object that was evaluated by the chain.
377
+ """
378
+ self.thing = thing
379
+ """The object that was evaluated by the chain."""
380
+ super(ChainExhaustedError, self).__init__('no rule in the chain matched and no default value is set')
381
+ # EngineError doesn't initialize the arguments so they would include *thing* when it's passed positionally, set
382
+ # them to the message to keep its contents out of str(), tracebacks and anything that serializes them
383
+ self.args = (self.message,)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rule-engine
3
- Version: 5.0.2
3
+ Version: 5.1.0
4
4
  Summary: A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
5
5
  Author-email: Spencer McIntyre <zeroSteiner@gmail.com>
6
6
  Maintainer-email: Spencer McIntyre <zeroSteiner@gmail.com>
@@ -45,6 +45,7 @@ Python with some inspiration from Ruby. Some features of this language includes:
45
45
  - Compound datatypes (equivalents for Python dict, list and set types)
46
46
  - Data attributes
47
47
  - Thread safety
48
+ - Rule chains that return the value of the first matching rule
48
49
 
49
50
  Example Usage
50
51
  -------------
@@ -30,6 +30,7 @@ lib/rule_engine/ast/expression/control.py
30
30
  lib/rule_engine/ast/expression/resolution.py
31
31
  lib/rule_engine/engine/__init__.py
32
32
  lib/rule_engine/engine/_attribute_resolver.py
33
+ lib/rule_engine/engine/chain.py
33
34
  lib/rule_engine/engine/context.py
34
35
  lib/rule_engine/engine/rule.py
35
36
  lib/rule_engine/parser/__init__.py
File without changes
File without changes
File without changes