|
|
|
@@ -1,5 +1,5 @@ |
|
|
|
from collections.abc import Callable, Sequence |
|
|
|
from typing import Literal |
|
|
|
from typing import Literal, Union |
|
|
|
|
|
|
|
from core.file import File |
|
|
|
from core.variables import ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment |
|
|
|
@@ -9,6 +9,7 @@ from core.workflow.nodes.enums import NodeType |
|
|
|
from models.workflow import WorkflowNodeExecutionStatus |
|
|
|
|
|
|
|
from .entities import ListOperatorNodeData |
|
|
|
from .exc import InvalidConditionError, InvalidFilterValueError, InvalidKeyError, ListOperatorError |
|
|
|
|
|
|
|
|
|
|
|
class ListOperatorNode(BaseNode[ListOperatorNodeData]): |
|
|
|
@@ -26,7 +27,17 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): |
|
|
|
return NodeRunResult( |
|
|
|
status=WorkflowNodeExecutionStatus.FAILED, error=error_message, inputs=inputs, outputs=outputs |
|
|
|
) |
|
|
|
if variable.value and not isinstance(variable, ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment): |
|
|
|
if not variable.value: |
|
|
|
inputs = {"variable": []} |
|
|
|
process_data = {"variable": []} |
|
|
|
outputs = {"result": [], "first_record": None, "last_record": None} |
|
|
|
return NodeRunResult( |
|
|
|
status=WorkflowNodeExecutionStatus.SUCCEEDED, |
|
|
|
inputs=inputs, |
|
|
|
process_data=process_data, |
|
|
|
outputs=outputs, |
|
|
|
) |
|
|
|
if not isinstance(variable, ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment): |
|
|
|
error_message = ( |
|
|
|
f"Variable {self.node_data.variable} is not an ArrayFileSegment, ArrayNumberSegment " |
|
|
|
"or ArrayStringSegment" |
|
|
|
@@ -36,70 +47,98 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): |
|
|
|
) |
|
|
|
|
|
|
|
if isinstance(variable, ArrayFileSegment): |
|
|
|
inputs = {"variable": [item.to_dict() for item in variable.value]} |
|
|
|
process_data["variable"] = [item.to_dict() for item in variable.value] |
|
|
|
else: |
|
|
|
inputs = {"variable": variable.value} |
|
|
|
process_data["variable"] = variable.value |
|
|
|
|
|
|
|
# Filter |
|
|
|
if self.node_data.filter_by.enabled: |
|
|
|
for condition in self.node_data.filter_by.conditions: |
|
|
|
if isinstance(variable, ArrayStringSegment): |
|
|
|
if not isinstance(condition.value, str): |
|
|
|
raise ValueError(f"Invalid filter value: {condition.value}") |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
filter_func = _get_string_filter_func(condition=condition.comparison_operator, value=value) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayNumberSegment): |
|
|
|
if not isinstance(condition.value, str): |
|
|
|
raise ValueError(f"Invalid filter value: {condition.value}") |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
filter_func = _get_number_filter_func(condition=condition.comparison_operator, value=float(value)) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayFileSegment): |
|
|
|
if isinstance(condition.value, str): |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
else: |
|
|
|
value = condition.value |
|
|
|
filter_func = _get_file_filter_func( |
|
|
|
key=condition.key, |
|
|
|
condition=condition.comparison_operator, |
|
|
|
value=value, |
|
|
|
) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
|
|
|
|
# Order |
|
|
|
if self.node_data.order_by.enabled: |
|
|
|
try: |
|
|
|
# Filter |
|
|
|
if self.node_data.filter_by.enabled: |
|
|
|
variable = self._apply_filter(variable) |
|
|
|
|
|
|
|
# Order |
|
|
|
if self.node_data.order_by.enabled: |
|
|
|
variable = self._apply_order(variable) |
|
|
|
|
|
|
|
# Slice |
|
|
|
if self.node_data.limit.enabled: |
|
|
|
variable = self._apply_slice(variable) |
|
|
|
|
|
|
|
outputs = { |
|
|
|
"result": variable.value, |
|
|
|
"first_record": variable.value[0] if variable.value else None, |
|
|
|
"last_record": variable.value[-1] if variable.value else None, |
|
|
|
} |
|
|
|
return NodeRunResult( |
|
|
|
status=WorkflowNodeExecutionStatus.SUCCEEDED, |
|
|
|
inputs=inputs, |
|
|
|
process_data=process_data, |
|
|
|
outputs=outputs, |
|
|
|
) |
|
|
|
except ListOperatorError as e: |
|
|
|
return NodeRunResult( |
|
|
|
status=WorkflowNodeExecutionStatus.FAILED, |
|
|
|
error=str(e), |
|
|
|
inputs=inputs, |
|
|
|
process_data=process_data, |
|
|
|
outputs=outputs, |
|
|
|
) |
|
|
|
|
|
|
|
def _apply_filter( |
|
|
|
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] |
|
|
|
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: |
|
|
|
for condition in self.node_data.filter_by.conditions: |
|
|
|
if isinstance(variable, ArrayStringSegment): |
|
|
|
result = _order_string(order=self.node_data.order_by.value, array=variable.value) |
|
|
|
if not isinstance(condition.value, str): |
|
|
|
raise InvalidFilterValueError(f"Invalid filter value: {condition.value}") |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
filter_func = _get_string_filter_func(condition=condition.comparison_operator, value=value) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayNumberSegment): |
|
|
|
result = _order_number(order=self.node_data.order_by.value, array=variable.value) |
|
|
|
if not isinstance(condition.value, str): |
|
|
|
raise InvalidFilterValueError(f"Invalid filter value: {condition.value}") |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
filter_func = _get_number_filter_func(condition=condition.comparison_operator, value=float(value)) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayFileSegment): |
|
|
|
result = _order_file( |
|
|
|
order=self.node_data.order_by.value, order_by=self.node_data.order_by.key, array=variable.value |
|
|
|
if isinstance(condition.value, str): |
|
|
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text |
|
|
|
else: |
|
|
|
value = condition.value |
|
|
|
filter_func = _get_file_filter_func( |
|
|
|
key=condition.key, |
|
|
|
condition=condition.comparison_operator, |
|
|
|
value=value, |
|
|
|
) |
|
|
|
result = list(filter(filter_func, variable.value)) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
return variable |
|
|
|
|
|
|
|
# Slice |
|
|
|
if self.node_data.limit.enabled: |
|
|
|
result = variable.value[: self.node_data.limit.size] |
|
|
|
def _apply_order( |
|
|
|
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] |
|
|
|
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: |
|
|
|
if isinstance(variable, ArrayStringSegment): |
|
|
|
result = _order_string(order=self.node_data.order_by.value, array=variable.value) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayNumberSegment): |
|
|
|
result = _order_number(order=self.node_data.order_by.value, array=variable.value) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
elif isinstance(variable, ArrayFileSegment): |
|
|
|
result = _order_file( |
|
|
|
order=self.node_data.order_by.value, order_by=self.node_data.order_by.key, array=variable.value |
|
|
|
) |
|
|
|
variable = variable.model_copy(update={"value": result}) |
|
|
|
return variable |
|
|
|
|
|
|
|
outputs = { |
|
|
|
"result": variable.value, |
|
|
|
"first_record": variable.value[0] if variable.value else None, |
|
|
|
"last_record": variable.value[-1] if variable.value else None, |
|
|
|
} |
|
|
|
return NodeRunResult( |
|
|
|
status=WorkflowNodeExecutionStatus.SUCCEEDED, |
|
|
|
inputs=inputs, |
|
|
|
process_data=process_data, |
|
|
|
outputs=outputs, |
|
|
|
) |
|
|
|
def _apply_slice( |
|
|
|
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] |
|
|
|
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: |
|
|
|
result = variable.value[: self.node_data.limit.size] |
|
|
|
return variable.model_copy(update={"value": result}) |
|
|
|
|
|
|
|
|
|
|
|
def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]: |
|
|
|
@@ -107,7 +146,7 @@ def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]: |
|
|
|
case "size": |
|
|
|
return lambda x: x.size |
|
|
|
case _: |
|
|
|
raise ValueError(f"Invalid key: {key}") |
|
|
|
raise InvalidKeyError(f"Invalid key: {key}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]: |
|
|
|
@@ -125,7 +164,7 @@ def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]: |
|
|
|
case "url": |
|
|
|
return lambda x: x.remote_url or "" |
|
|
|
case _: |
|
|
|
raise ValueError(f"Invalid key: {key}") |
|
|
|
raise InvalidKeyError(f"Invalid key: {key}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_string_filter_func(*, condition: str, value: str) -> Callable[[str], bool]: |
|
|
|
@@ -151,7 +190,7 @@ def _get_string_filter_func(*, condition: str, value: str) -> Callable[[str], bo |
|
|
|
case "not empty": |
|
|
|
return lambda x: x != "" |
|
|
|
case _: |
|
|
|
raise ValueError(f"Invalid condition: {condition}") |
|
|
|
raise InvalidConditionError(f"Invalid condition: {condition}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_sequence_filter_func(*, condition: str, value: Sequence[str]) -> Callable[[str], bool]: |
|
|
|
@@ -161,7 +200,7 @@ def _get_sequence_filter_func(*, condition: str, value: Sequence[str]) -> Callab |
|
|
|
case "not in": |
|
|
|
return lambda x: not _in(value)(x) |
|
|
|
case _: |
|
|
|
raise ValueError(f"Invalid condition: {condition}") |
|
|
|
raise InvalidConditionError(f"Invalid condition: {condition}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_number_filter_func(*, condition: str, value: int | float) -> Callable[[int | float], bool]: |
|
|
|
@@ -179,7 +218,7 @@ def _get_number_filter_func(*, condition: str, value: int | float) -> Callable[[ |
|
|
|
case "≥": |
|
|
|
return _ge(value) |
|
|
|
case _: |
|
|
|
raise ValueError(f"Invalid condition: {condition}") |
|
|
|
raise InvalidConditionError(f"Invalid condition: {condition}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str]) -> Callable[[File], bool]: |
|
|
|
@@ -193,7 +232,7 @@ def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str |
|
|
|
extract_func = _get_file_extract_number_func(key=key) |
|
|
|
return lambda x: _get_number_filter_func(condition=condition, value=float(value))(extract_func(x)) |
|
|
|
else: |
|
|
|
raise ValueError(f"Invalid key: {key}") |
|
|
|
raise InvalidKeyError(f"Invalid key: {key}") |
|
|
|
|
|
|
|
|
|
|
|
def _contains(value: str): |