Module pypowerautomate.actions.condition
Expand source code
import re
from typing import Dict
# Define operator precedence
precedence = {
"==": 4,
"!=": 4,
">": 4,
"<": 4,
">=": 4,
"<=": 4,
"not": 3,
"and": 2,
"or": 1
}
# Define operator mapping
operator_mapping = {
"==": "equals",
">=": "greaterOrEquals",
"<=": "lessOrEquals",
">": "greater",
"<": "less",
"and": "and",
"or": "or",
"not": "not",
"!=": "not_equals"
}
# Define the regular expression pattern for the tokenizer
# pattern = r"""
# "(?:\\.|[^\\"])*" # Match double quoted string with possible escaped double quotes
# |
# != # Match '!=' operator
# |
# == # Match '==' operator
# |
# >= # Match '>=' operator
# |
# <= # Match '<=' operator
# |
# [<>] # Match '<' or '>' operator
# |
# \b(?:and|or|not|true|false)\b # Match 'and', 'or', 'not' keywords
# |
# \( # Match '('
# |
# \) # Match ')'
# |
# \b\d*\.\d+\b # Match floating point numbers
# |
# \b\d+\b # Match integers
# |
# [a-zA-Z0-9_-]+ # Match variable names
# """
# Tokenizer function
# def tokenizer(s):
# return re.findall(pattern, s, re.VERBOSE)
def tokenizer(s):
"""Tokenizes the given string into a list of tokens.
This function takes a string as input and splits it into a list of tokens based on the following rules:
- Whitespace characters (spaces) are used to split the string into tokens, except within quoted strings.
- Operators (!=, ==, >=, <=, >, <) are treated as separate tokens.
- Parentheses '(' and ')' are treated as separate tokens.
- Quoted strings are treated as a single token, including the quotes.
- Digits, decimal points, and alphanumeric characters (including '-' and '_') are grouped into a single token.
- Keywords ('and', 'or', 'not', 'true', 'false') are treated as separate tokens if they are followed by a non-alphanumeric character or the end of the string.
- Raises a ValueError if an unknown character is encountered or if a quoted string is unterminated.
Args:
s (str): The input string to be tokenized.
Returns:
list: A list of tokens extracted from the input string.
"""
keywords = ['and', 'or', 'not', 'true', 'false']
operators = ['!=', '==', '>=', '<=', '>', '<']
tokens = []
token = ''
i = 0
while i < len(s):
char = s[i]
if char == ' ':
if token != '':
tokens.append(token)
token = ''
elif char in operators or (i + 1 < len(s) and s[i:i+2] in operators):
if token != '':
tokens.append(token)
token = ''
operator = s[i:i+2] if i + \
1 < len(s) and s[i:i+2] in operators else char
tokens.append(operator)
i += len(operator) - 1
elif char in ('(', ')'):
if token != '':
tokens.append(token)
token = ''
tokens.append(char)
elif char == '\"':
if token != '':
tokens.append(token)
token = ''
end_of_string = s.find('\"', i + 1)
if end_of_string == -1:
raise ValueError("Unterminated string literal")
token = s[i:end_of_string+1] # Include the quotes
tokens.append(token)
token = ''
i = end_of_string
# Digits and decimal point
elif char in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'):
token += char
elif char.isalpha() or char in ('-', '_'): # Variables and boolean values
token += char
# If this token is a keyword and is followed by a non-alphanumeric character or end of string, append it
if token in keywords and (i + 1 == len(s) or not s[i+1].isalnum()):
tokens.append(token)
token = ''
else:
raise ValueError("Unknown character: " + char)
i += 1
if token != '':
tokens.append(token)
return tokens
# Convert infix tokens to RPN using Shunting Yard algorithm
def infix_to_rpn(tokens):
"""
Converts an infix expression to reverse Polish notation (RPN).
This function takes a list of tokens representing an infix expression and converts it to a list of tokens in reverse Polish notation (RPN).
The conversion is done using the shunting-yard algorithm.
Args:
tokens (list): A list of tokens representing the infix expression.
Returns:
list: A list of tokens in reverse Polish notation (RPN).
"""
output_queue = []
operator_stack = []
for token in tokens:
if token in precedence: # If token is an operator
while (operator_stack and operator_stack[-1] in precedence and
precedence[token] <= precedence[operator_stack[-1]]):
output_queue.append(operator_stack.pop())
operator_stack.append(token)
elif token == "(":
operator_stack.append(token)
elif token == ")":
while operator_stack and operator_stack[-1] != "(":
output_queue.append(operator_stack.pop())
operator_stack.pop() # Remove the '('
else: # If token is a number or a variable
output_queue.append(token)
while operator_stack: # Pop remaining operators from the stack
output_queue.append(operator_stack.pop())
return output_queue
# Create AST from RPN
def create_ast(rpn_tokens):
"""
Constructs an abstract syntax tree (AST) from a list of tokens in reverse Polish notation (RPN).
This function takes a list of tokens in RPN and constructs an abstract syntax tree (AST) representing the expression.
The AST is represented as a dictionary with the following structure:
- Literal: {'type': 'Literal', 'value': value}
- BinaryExpression: {'type': 'BinaryExpression', 'operator': operator, 'left': left_node, 'right': right_node}
- UnaryExpression: {'type': 'UnaryExpression', 'operator': operator, 'argument': operand}
- LogicalExpression: {'type': 'LogicalExpression', 'operator': operator, 'left': left_node, 'right': right_node}
Args:
rpn_tokens (list): A list of tokens in reverse Polish notation (RPN).
Returns:
dict: The root node of the constructed abstract syntax tree (AST).
"""
stack = []
for token in rpn_tokens:
# If token is a number or a variable
if token not in precedence and token not in ["==", "!=", ">", "<", ">=", "<="]:
stack.append({"type": "Literal", "value": token})
else: # If token is an operator
if token in ["==", "!=", ">", "<", ">=", "<="]: # binary operators
right = stack.pop() if stack else None
left = stack.pop() if stack else None
if right is None or left is None:
raise ValueError(
'Malformed expression: insufficient operands for operator {}'.format(token))
stack.append({"type": "BinaryExpression",
"operator": token, "left": left, "right": right})
elif token == "not": # unary operator
operand = stack.pop() if stack else None
if operand is None:
raise ValueError(
'Malformed expression: insufficient operand for operator {}'.format(token))
stack.append({"type": "UnaryExpression",
"operator": token, "argument": operand})
else: # "and", "or" binary operators
right = stack.pop() if stack else None
left = stack.pop() if stack else None
if right is None or left is None:
raise ValueError(
'Malformed expression: insufficient operands for operator {}'.format(token))
stack.append({"type": "LogicalExpression",
"operator": token, "left": left, "right": right})
if len(stack) != 1:
raise ValueError(
'Malformed expression: the final stack contained more than one element')
return stack[0] # Root of the AST
def ast_to_dict(node):
"""
Converts an abstract syntax tree (AST) node to a dictionary representation.
This function takes an AST node, which is represented as a dictionary, and converts it to a dictionary representation that can be easily serialized to JSON.
The conversion is based on the node type:
- Literal: Converts the value to the appropriate data type (boolean, number, or string).
- UnaryExpression: Converts the operator and argument to a dictionary with the operator as the key and the argument as the value.
- BinaryExpression and LogicalExpression: Converts the operator and operands to a dictionary with the operator as the key and the operands as a list.
Args:
node (dict): The input AST node to be converted.
Returns:
dict: The dictionary representation of the AST node.
"""
node_type = node["type"]
if node_type == "Literal":
value = node["value"]
if value == "true":
return True
elif value == "false":
return False
elif "\"" in value:
value = value.replace("\"", "")
return value
try:
value = float(value) if '.' in value else int(value)
except ValueError:
value = f'@variables(\'{value}\')'
return value
elif node_type == "UnaryExpression":
operator = operator_mapping[node["operator"]]
argument = ast_to_dict(node["argument"])
return {operator: argument}
elif node_type in ["BinaryExpression", "LogicalExpression"]:
operator = operator_mapping[node["operator"]]
left = ast_to_dict(node["left"])
right = ast_to_dict(node["right"])
# Handle "not equals" operator
if operator == "not_equals":
return {"not": {"equals": [left, right]}}
else:
return {operator: [left, right]}
class Condition:
"""
A class for defining condition expressions.
This class provides a way to define condition expressions and convert them to a JSON-compatible dictionary representation.
The condition expression is first tokenized, then converted to reverse Polish notation (RPN), and finally an abstract syntax tree (AST) is constructed.
The `export()` method returns the dictionary representation of the condition expression.
Example:
condition = Condition("var2 == false")
print(condition.export())
# Output: {'expression': {'equals': ['@variables(\'var2\')', False]}}
Args:
expression (str): The condition expression to be defined.
"""
def __init__(self, expression: str) -> None:
tokens = tokenizer(expression)
rpn_tokens = infix_to_rpn(tokens)
self.ast = create_ast(rpn_tokens)
def export(self) -> Dict:
return ast_to_dict(self.ast)
# s = '(var == "test" and var2 != "test2") or var3 > 1.22 or var4 < 2 or var5 >= 0.123 or var6 <= 6 or not var7 == 7'
# s = 'var == 1 or (var == "test" and var2 != "test2") or var3 > 1.22 or var4 < 2 or var5 >= 0.123 or var6 <= 6 or not var7 == 7'
# s = 'a == true or b != false'
# C = Condition("(aaaho31lk1j2109 == .209122 ) and (Var2221 >= 10) and (false or var1 == (var02 == false))")
# print(C.export())
Functions
def ast_to_dict(node)
-
Converts an abstract syntax tree (AST) node to a dictionary representation.
This function takes an AST node, which is represented as a dictionary, and converts it to a dictionary representation that can be easily serialized to JSON. The conversion is based on the node type: - Literal: Converts the value to the appropriate data type (boolean, number, or string). - UnaryExpression: Converts the operator and argument to a dictionary with the operator as the key and the argument as the value. - BinaryExpression and LogicalExpression: Converts the operator and operands to a dictionary with the operator as the key and the operands as a list.
Args
node
:dict
- The input AST node to be converted.
Returns
dict
- The dictionary representation of the AST node.
Expand source code
def ast_to_dict(node): """ Converts an abstract syntax tree (AST) node to a dictionary representation. This function takes an AST node, which is represented as a dictionary, and converts it to a dictionary representation that can be easily serialized to JSON. The conversion is based on the node type: - Literal: Converts the value to the appropriate data type (boolean, number, or string). - UnaryExpression: Converts the operator and argument to a dictionary with the operator as the key and the argument as the value. - BinaryExpression and LogicalExpression: Converts the operator and operands to a dictionary with the operator as the key and the operands as a list. Args: node (dict): The input AST node to be converted. Returns: dict: The dictionary representation of the AST node. """ node_type = node["type"] if node_type == "Literal": value = node["value"] if value == "true": return True elif value == "false": return False elif "\"" in value: value = value.replace("\"", "") return value try: value = float(value) if '.' in value else int(value) except ValueError: value = f'@variables(\'{value}\')' return value elif node_type == "UnaryExpression": operator = operator_mapping[node["operator"]] argument = ast_to_dict(node["argument"]) return {operator: argument} elif node_type in ["BinaryExpression", "LogicalExpression"]: operator = operator_mapping[node["operator"]] left = ast_to_dict(node["left"]) right = ast_to_dict(node["right"]) # Handle "not equals" operator if operator == "not_equals": return {"not": {"equals": [left, right]}} else: return {operator: [left, right]}
def create_ast(rpn_tokens)
-
Constructs an abstract syntax tree (AST) from a list of tokens in reverse Polish notation (RPN).
This function takes a list of tokens in RPN and constructs an abstract syntax tree (AST) representing the expression. The AST is represented as a dictionary with the following structure: - Literal: {'type': 'Literal', 'value': value} - BinaryExpression: {'type': 'BinaryExpression', 'operator': operator, 'left': left_node, 'right': right_node} - UnaryExpression: {'type': 'UnaryExpression', 'operator': operator, 'argument': operand} - LogicalExpression: {'type': 'LogicalExpression', 'operator': operator, 'left': left_node, 'right': right_node}
Args
rpn_tokens
:list
- A list of tokens in reverse Polish notation (RPN).
Returns
dict
- The root node of the constructed abstract syntax tree (AST).
Expand source code
def create_ast(rpn_tokens): """ Constructs an abstract syntax tree (AST) from a list of tokens in reverse Polish notation (RPN). This function takes a list of tokens in RPN and constructs an abstract syntax tree (AST) representing the expression. The AST is represented as a dictionary with the following structure: - Literal: {'type': 'Literal', 'value': value} - BinaryExpression: {'type': 'BinaryExpression', 'operator': operator, 'left': left_node, 'right': right_node} - UnaryExpression: {'type': 'UnaryExpression', 'operator': operator, 'argument': operand} - LogicalExpression: {'type': 'LogicalExpression', 'operator': operator, 'left': left_node, 'right': right_node} Args: rpn_tokens (list): A list of tokens in reverse Polish notation (RPN). Returns: dict: The root node of the constructed abstract syntax tree (AST). """ stack = [] for token in rpn_tokens: # If token is a number or a variable if token not in precedence and token not in ["==", "!=", ">", "<", ">=", "<="]: stack.append({"type": "Literal", "value": token}) else: # If token is an operator if token in ["==", "!=", ">", "<", ">=", "<="]: # binary operators right = stack.pop() if stack else None left = stack.pop() if stack else None if right is None or left is None: raise ValueError( 'Malformed expression: insufficient operands for operator {}'.format(token)) stack.append({"type": "BinaryExpression", "operator": token, "left": left, "right": right}) elif token == "not": # unary operator operand = stack.pop() if stack else None if operand is None: raise ValueError( 'Malformed expression: insufficient operand for operator {}'.format(token)) stack.append({"type": "UnaryExpression", "operator": token, "argument": operand}) else: # "and", "or" binary operators right = stack.pop() if stack else None left = stack.pop() if stack else None if right is None or left is None: raise ValueError( 'Malformed expression: insufficient operands for operator {}'.format(token)) stack.append({"type": "LogicalExpression", "operator": token, "left": left, "right": right}) if len(stack) != 1: raise ValueError( 'Malformed expression: the final stack contained more than one element') return stack[0] # Root of the AST
def infix_to_rpn(tokens)
-
Converts an infix expression to reverse Polish notation (RPN).
This function takes a list of tokens representing an infix expression and converts it to a list of tokens in reverse Polish notation (RPN). The conversion is done using the shunting-yard algorithm.
Args
tokens
:list
- A list of tokens representing the infix expression.
Returns
list
- A list of tokens in reverse Polish notation (RPN).
Expand source code
def infix_to_rpn(tokens): """ Converts an infix expression to reverse Polish notation (RPN). This function takes a list of tokens representing an infix expression and converts it to a list of tokens in reverse Polish notation (RPN). The conversion is done using the shunting-yard algorithm. Args: tokens (list): A list of tokens representing the infix expression. Returns: list: A list of tokens in reverse Polish notation (RPN). """ output_queue = [] operator_stack = [] for token in tokens: if token in precedence: # If token is an operator while (operator_stack and operator_stack[-1] in precedence and precedence[token] <= precedence[operator_stack[-1]]): output_queue.append(operator_stack.pop()) operator_stack.append(token) elif token == "(": operator_stack.append(token) elif token == ")": while operator_stack and operator_stack[-1] != "(": output_queue.append(operator_stack.pop()) operator_stack.pop() # Remove the '(' else: # If token is a number or a variable output_queue.append(token) while operator_stack: # Pop remaining operators from the stack output_queue.append(operator_stack.pop()) return output_queue
def tokenizer(s)
-
Tokenizes the given string into a list of tokens.
This function takes a string as input and splits it into a list of tokens based on the following rules:
- Whitespace characters (spaces) are used to split the string into tokens, except within quoted strings.
- Operators (!=, ==, >=, <=, >, <) are treated as separate tokens.
- Parentheses '(' and ')' are treated as separate tokens.
- Quoted strings are treated as a single token, including the quotes.
- Digits, decimal points, and alphanumeric characters (including '-' and '_') are grouped into a single token.
- Keywords ('and', 'or', 'not', 'true', 'false') are treated as separate tokens if they are followed by a non-alphanumeric character or the end of the string.
- Raises a ValueError if an unknown character is encountered or if a quoted string is unterminated.
Args
s
:str
- The input string to be tokenized.
Returns
list
- A list of tokens extracted from the input string.
Expand source code
def tokenizer(s): """Tokenizes the given string into a list of tokens. This function takes a string as input and splits it into a list of tokens based on the following rules: - Whitespace characters (spaces) are used to split the string into tokens, except within quoted strings. - Operators (!=, ==, >=, <=, >, <) are treated as separate tokens. - Parentheses '(' and ')' are treated as separate tokens. - Quoted strings are treated as a single token, including the quotes. - Digits, decimal points, and alphanumeric characters (including '-' and '_') are grouped into a single token. - Keywords ('and', 'or', 'not', 'true', 'false') are treated as separate tokens if they are followed by a non-alphanumeric character or the end of the string. - Raises a ValueError if an unknown character is encountered or if a quoted string is unterminated. Args: s (str): The input string to be tokenized. Returns: list: A list of tokens extracted from the input string. """ keywords = ['and', 'or', 'not', 'true', 'false'] operators = ['!=', '==', '>=', '<=', '>', '<'] tokens = [] token = '' i = 0 while i < len(s): char = s[i] if char == ' ': if token != '': tokens.append(token) token = '' elif char in operators or (i + 1 < len(s) and s[i:i+2] in operators): if token != '': tokens.append(token) token = '' operator = s[i:i+2] if i + \ 1 < len(s) and s[i:i+2] in operators else char tokens.append(operator) i += len(operator) - 1 elif char in ('(', ')'): if token != '': tokens.append(token) token = '' tokens.append(char) elif char == '\"': if token != '': tokens.append(token) token = '' end_of_string = s.find('\"', i + 1) if end_of_string == -1: raise ValueError("Unterminated string literal") token = s[i:end_of_string+1] # Include the quotes tokens.append(token) token = '' i = end_of_string # Digits and decimal point elif char in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'): token += char elif char.isalpha() or char in ('-', '_'): # Variables and boolean values token += char # If this token is a keyword and is followed by a non-alphanumeric character or end of string, append it if token in keywords and (i + 1 == len(s) or not s[i+1].isalnum()): tokens.append(token) token = '' else: raise ValueError("Unknown character: " + char) i += 1 if token != '': tokens.append(token) return tokens
Classes
class Condition (expression: str)
-
A class for defining condition expressions.
This class provides a way to define condition expressions and convert them to a JSON-compatible dictionary representation. The condition expression is first tokenized, then converted to reverse Polish notation (RPN), and finally an abstract syntax tree (AST) is constructed. The
export()
method returns the dictionary representation of the condition expression.Example
condition = Condition("var2 == false") print(condition.export())
Output: {'expression': {'equals': ['@variables('var2')', False]}}
Args
expression
:str
- The condition expression to be defined.
Expand source code
class Condition: """ A class for defining condition expressions. This class provides a way to define condition expressions and convert them to a JSON-compatible dictionary representation. The condition expression is first tokenized, then converted to reverse Polish notation (RPN), and finally an abstract syntax tree (AST) is constructed. The `export()` method returns the dictionary representation of the condition expression. Example: condition = Condition("var2 == false") print(condition.export()) # Output: {'expression': {'equals': ['@variables(\'var2\')', False]}} Args: expression (str): The condition expression to be defined. """ def __init__(self, expression: str) -> None: tokens = tokenizer(expression) rpn_tokens = infix_to_rpn(tokens) self.ast = create_ast(rpn_tokens) def export(self) -> Dict: return ast_to_dict(self.ast)
Methods
def export(self) ‑> Dict
-
Expand source code
def export(self) -> Dict: return ast_to_dict(self.ast)