Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git branch:*)",
"Bash(git show:*)",
"Bash(gh issue view:*)",
"Bash(gh pr view:*)",
"Glob",
Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[build-system]
requires = ["hatchling", "hatch-requirements-txt"]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.setuptools]
packages = ["object_filtering"]

[project]
name = "object_filtering"
version = "0.3.0"
version = "0.4.0"
authors = [
{ name="Scott Ratchford", email="object_filtering@scottratchford.com" },
]
Expand All @@ -28,7 +28,9 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities",
]
dynamic = ["dependencies"]
dependencies = [
"numpy>=2.0.0",
]

[project.urls]
Homepage = "https://github.com/KyberCritter/Object-Filtering"
Expand All @@ -43,6 +45,3 @@ dev = [
pythonpath = [
"."
]

[tool.hatch.metadata.hooks.requirements_txt]
files = ["requirements.txt"]
1 change: 0 additions & 1 deletion requirements.txt

This file was deleted.

132 changes: 62 additions & 70 deletions src/object_filtering/object_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,16 +481,14 @@ def get_logical_expression_type(

raise ValueError("expression is not a LogicalExpression.")

def is_logical_expression_valid(expression: LogicalExpression, obj: Any = None) -> bool:
def is_logical_expression_valid(expression: LogicalExpression) -> bool:
"""Determines whether a LogicalExpression conforms to the format from the
documentation.

Args:
expression (LogicalExpression): The LogicalExpression to check the
validity of.
obj (Any): The object that will be filtered. All criteria in the Rules
must be present and whitelisted for its type. Defaults to None. If
None, criteria validity checks will be skipped.
validity of. A plain dict is implicitly converted to the
appropriate _LogicalExpressionBase subclass.

Raises:
ValueError: If expression is not a LogicalExpression
Expand All @@ -505,29 +503,26 @@ def is_logical_expression_valid(expression: LogicalExpression, obj: Any = None)
# True and False are both valid
return True
elif expr_type == Rule:
return is_rule_valid(expression, obj)
return is_rule_valid(expression)
elif expr_type == ConditionalExpression:
return is_conditional_expression_valid(expression, obj)
return is_conditional_expression_valid(expression)
elif expr_type == GroupExpression:
return is_group_expression_valid(expression, obj)
return is_group_expression_valid(expression)
elif expr_type == ObjectFilter:
return is_filter_valid(expression, obj)
return is_filter_valid(expression)
else:
raise ValueError("expression is not a LogicalExpression.")

def is_rule_valid(rule: dict, obj: Any = None) -> bool:
def is_rule_valid(rule: dict | Rule) -> bool:
"""Determines whether a rule conforms to the format from the documentation.
All methods used as criteria must be decorated with @filter_criterion.
Raises an error if the rule is not valid.

Args:
rule (dict): The rule to check the validity of.
obj (Any): The object that will be filtered.
All criteria in the rules must be present and whitelisted for its type.
Defaults to None.
rule (dict | Rule): The rule to check the validity of. A plain dict is
implicitly converted to a Rule object.

Returns:
bool: Whether the rule is valid
bool: Whether the rule is valid.

- Required keys and their values' data types:
- criterion (str): The variable or method to compare against.
Expand All @@ -550,44 +545,29 @@ def is_rule_valid(rule: dict, obj: Any = None) -> bool:
raise FilterError("rule parameter is not a list.")
if not isinstance(rule["multi_value_behavior"], str):
raise FilterError("rule multi_value_behavior is not a string.")

if obj is not None:
# value checks
if rule["operator"].upper() not in VALID_OPERATORS:
raise FilterError("rule operator is not a valid operator.")
# special variable handling
if rule["criterion"] in SPECIAL_VARIABLES:
if rule["criterion"] == "$CLASS$" and rule["operator"] not in CLASS_VARIABLE_OPERATORS:
raise FilterError("$CLASS$ only supports == and != operators.")
return True
try: # check if method exists
method = getattr(obj, rule["criterion"])
except:
raise FilterError(f"method {rule['criterion']} does not exist in obj.")
# check if method is decorated with @filter_criterion
if not isinstance(obj, ObjectWrapper):
if callable(method) and not hasattr(method, "_is_whitelisted"):
raise FilterError(f"method {rule['criterion']} is not whitelisted in obj. No _is_whitelisted method.")
if hasattr(method, "_is_whitelisted") and not method._is_whitelisted:
raise FilterError(f"method {rule['criterion']} is not whitelisted in obj.")
if rule["multi_value_behavior"] not in VALID_MULTI_VALUE_BEHAVIORS:
raise FilterError(f"rule multi_value_behavior is not a valid multi_value_behavior.")
# value checks
if rule["operator"].upper() not in VALID_OPERATORS:
raise FilterError("rule operator is not a valid operator.")
# special variable handling
if rule["criterion"] in SPECIAL_VARIABLES:
if rule["criterion"] == "$CLASS$" and rule["operator"] not in CLASS_VARIABLE_OPERATORS:
raise FilterError("$CLASS$ only supports == and != operators.")

return True

def is_conditional_expression_valid(expression: ConditionalExpression, obj: Any = None) -> bool:
def is_conditional_expression_valid(expression: dict | ConditionalExpression) -> bool:
"""Determines whether a ConditionalExpression conforms to the format from
the documentation. Raises an error if the ConditionalExpression is not
valid.

Args:
expression (dict): The ConditionalExpression to check the validity of.
obj (Any): The object that will be filtered. All criteria in the Rules
must be present and whitelisted for its type. Defaults to None.
expression (dict | ConditionalExpression): The ConditionalExpression to
check the validity of. A plain dict is implicitly converted to a
ConditionalExpression object.

Returns:
bool: Whether the conditional expression is valid.

- Required keys and their values' data types:
- if (LogicalExpression): The first LogicalExpression to evaluate
- then (LogicalExpression): The LogicalExpression to evaluate if the
Expand All @@ -599,21 +579,20 @@ def is_conditional_expression_valid(expression: ConditionalExpression, obj: Any
expression = dict_to_logical_expression(expression)
if get_logical_expression_type(expression) != ConditionalExpression:
raise FilterError("expression is not a ConditionalExpression.")
return all([is_logical_expression_valid(exp, obj) for exp in expression.values()])
return all([is_logical_expression_valid(exp) for exp in expression.values()])

def is_group_expression_valid(expression: GroupExpression, obj: Any = None) -> bool:
def is_group_expression_valid(expression: GroupExpression) -> bool:
"""Determines whether the GroupExpression conforms to the format from the
documentation.

Args:
expression (GroupExpression): The GroupExpression to check the validity
of.
obj (Any): The object that will be filtered. All criteria in the Rules
must be present and whitelisted for its type. Defaults to None.
of. A plain dict is implicitly converted to the appropriate
_LogicalExpressionBase subclass.

Returns:
bool: Whether the GroupExpression is valid.

- Required keys and their values' data types:
- logical_operator (str): "and" or "or"
- logical_expressions (list[LogicalExpression]): The LogicalExpressions
Expand All @@ -623,14 +602,15 @@ def is_group_expression_valid(expression: GroupExpression, obj: Any = None) -> b
expression = dict_to_logical_expression(expression)
if not expression["logical_operator"] in VALID_LOGICAL_OPERATORS: # must be "and" or "or"
raise FilterError("expression logical_operator is not a valid logical operator.")
return all([is_logical_expression_valid(exp, obj) for exp in expression["logical_expressions"]])
return all([is_logical_expression_valid(exp) for exp in expression["logical_expressions"]])

def is_filter_valid(filter: ObjectFilter, obj: Any = None) -> bool:
"""Determines whether an ObjectFilter conforms to the format from the
documentation.

Args:
filter (ObjectFilter): The ObjectFilter to check the validity of.
filter (ObjectFilter): The ObjectFilter to check the validity of. A
plain dict is implicitly converted to an ObjectFilter object.
obj (Any): The object that will be filtered. All criteria in the Rules
must be present and whitelisted for its type. Defaults to None.

Expand Down Expand Up @@ -674,7 +654,7 @@ def is_filter_valid(filter: ObjectFilter, obj: Any = None) -> bool:
if filter["priority"] < 0:
raise ValueError("filter priority is less than 0.")

if not is_logical_expression_valid(filter["logical_expression"], obj):
if not is_logical_expression_valid(filter["logical_expression"]):
return False
# validate obj type
if obj is not None and not type_name_matches(obj, filter["object_types"]):
Expand All @@ -692,7 +672,8 @@ def sanitize_filter(filter: ObjectFilter) -> ObjectFilter:
altered deep copy while preserving the original.

Args:
filter (ObjectFilter): The ObjectFilter to sanitize.
filter (ObjectFilter): The ObjectFilter to sanitize. A plain dict is
implicitly converted to an ObjectFilter object.

Raises:
TypeError: If the ObjectFilter is not a dict.
Expand All @@ -716,7 +697,7 @@ def sanitize_filter(filter: ObjectFilter) -> ObjectFilter:
sanitized[key] = value # Keep other data types unchanged
return sanitized

def get_value(obj: Any, rule: dict) -> Any:
def get_value(obj: Any, rule: dict | Rule) -> Any:
"""Returns the value of an attribute of `obj`, based on
`rule["criterion"]`.

Expand All @@ -727,7 +708,8 @@ def get_value(obj: Any, rule: dict) -> Any:
Args:
obj (Any): The object that the rule will be executed with. All criteria
in the rules must be present and whitelisted for its type.
rule (dict): The rule to execute.
rule (dict | Rule): The rule to execute. A plain dict is implicitly
converted to a Rule object.

Raises:
ValueError: If the criterion is a method without `@filter_criterion`.
Expand Down Expand Up @@ -768,14 +750,16 @@ def get_value(obj: Any, rule: dict) -> Any:
Execution Functions
"""

def execute_logical_expression_on_object(obj: Any, expression: LogicalExpression) -> bool:
def execute_logical_expression_on_object(obj: Any, expression: dict | LogicalExpression) -> bool:
"""Executes a LogicalExpression on an object.

Args:
obj (Any): The object that the LogicalExpression will be executed with.
All criteria in the Rules must be present and whitelisted for its
type.
expression (LogicalExpression): The LogicalExpression to execute.
expression (dict | LogicalExpression): The LogicalExpression to
execute. A plain dict is implicitly converted to the appropriate
_LogicalExpressionBase subclass.

Raises:
ValueError: If expression is not a LogicalExpression
Expand Down Expand Up @@ -833,7 +817,8 @@ def execute_rule_on_object(obj: Any, rule: dict) -> bool:
Args:
obj (Any): The object that the rule will be executed with. All criteria
in the rules must be present and whitelisted for its type.
rule (dict): The rule to execute.
rule (dict): The rule to execute. A plain dict is implicitly converted
to the appropriate _LogicalExpressionBase subclass.

Raises:
ValueError: If rule["comparison_value"] is not valid.
Expand Down Expand Up @@ -879,7 +864,9 @@ def execute_conditional_expression_on_object(obj: Any, expression: dict) -> bool
Args:
obj (Any): The object that the conditional expression will be executed with.
All criteria in the rules must be present and whitelisted for its type.
expression (dict): The conditional expression to execute.
expression (dict): The conditional expression to execute. A plain dict
is implicitly converted to the appropriate _LogicalExpressionBase
subclass.

Raises:
ValueError: If expression does not match the format of a conditional expression.
Expand All @@ -902,7 +889,9 @@ def execute_group_expression_on_object(obj: Any, expression: GroupExpression) ->
Args:
obj (Any): The object that the GroupExpression will be executed with.
All criteria in the rules must be present and whitelisted for its type.
expression (GroupExpression): The GroupExpression to execute.
expression (GroupExpression): The GroupExpression to execute. A plain
dict is implicitly converted to the appropriate
_LogicalExpressionBase subclass.

Raises:
ValueError: If expression does not match the format of a GroupExpression.
Expand All @@ -928,7 +917,9 @@ def execute_filter_on_object(obj, filter: ObjectFilter, sanitize: bool = True) -

Args:
obj: Any object.
filter (ObjectFilter): An ObjectFilter to execute.
filter (ObjectFilter): An ObjectFilter to execute. A plain dict is
implicitly converted to the appropriate _LogicalExpressionBase
subclass.
sanitize (bool, optional): Whether or not to santize the ObjectFilters
before execution. Defaults to True.

Expand All @@ -955,7 +946,9 @@ def execute_filter_on_array(obj_array: np.ndarray[Any], filter: dict, sanitize:

Args:
obj_array (np.ndarray[Any]): Array of any type of object.
filter (ObjectFilter): An ObjectFilter to execute.
filter (ObjectFilter): An ObjectFilter to execute. A plain dict is
implicitly converted to the appropriate _LogicalExpressionBase
subclass.
sanitize (bool, optional): Whether or not to santize the ObjectFilters
before execution. Defaults to True.

Expand Down Expand Up @@ -996,7 +989,8 @@ def execute_filter_list_on_object(
Args:
obj (Any): Any object.
filter_list (list[ObjectFilter]): A list of ObjectFilter to execute on
`obj`.
`obj`. Plain dicts in the list are implicitly converted to the
appropriate _LogicalExpressionBase subclass.
sanitize (bool, optional): Whether or not to santize the ObjectFilters
before execution. Defaults to True.

Expand All @@ -1021,7 +1015,8 @@ def execute_filter_list_on_array(
Args:
obj_array (np.ndarray[Any]): Array of any type of object.
filter_list (list[dict]): A list of ObjectFilters to execute on the
elements of `obj_array`.
elements of `obj_array`. Plain dicts in the list are implicitly
converted to the appropriate _LogicalExpressionBase subclass.
sanitize (bool, optional): Whether or not to santize the ObjectFilters
before execution. Defaults to True.

Expand Down Expand Up @@ -1052,7 +1047,8 @@ def execute_filter_list_on_object_get_first_success(
Args:
obj (Any): Any object.
filter_list (list[ObjectFilter]): A list of ObjectFilters to execute
on `obj`.
on `obj`. Plain dicts in the list are implicitly converted to the
appropriate _LogicalExpressionBase subclass.
sanitize (bool, optional): Whether or not to santize the ObjectFilters
before execution. Defaults to True.

Expand Down Expand Up @@ -1096,11 +1092,7 @@ def method(*args, **kwargs):
raise AttributeError(f"Not all objects have the attribute '{name}'")
else:
if hasattr(self._obj, name):
attr = getattr(self._obj, name)
# If the attribute is a method, return it directly
if callable(attr):
return attr
else:
return attr
return getattr(self._obj, name)
else:
raise AttributeError(f"'{type(self._obj).__name__}' object has no attribute '{name}'")
Loading