I can run simple workflow
This commit is contained in:
@@ -184,11 +184,13 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
WorkflowPlayer,
|
WorkflowPlayer,
|
||||||
settings_manager=self._settings_manager,
|
settings_manager=self._settings_manager,
|
||||||
tabs_manager=self.tabs_manager,
|
tabs_manager=self.tabs_manager,
|
||||||
player_settings=WorkflowsPlayerSettings(workflow_name),
|
player_settings=WorkflowsPlayerSettings(workflow_name,
|
||||||
|
list(self._state.components.values())),
|
||||||
boundaries=boundaries)
|
boundaries=boundaries)
|
||||||
|
|
||||||
|
player.run()
|
||||||
|
|
||||||
self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
|
self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
|
||||||
# player.start(self._state.selected_component_id)
|
|
||||||
return self.tabs_manager.refresh()
|
return self.tabs_manager.refresh()
|
||||||
|
|
||||||
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
||||||
@@ -436,9 +438,9 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
Fieldset(
|
Fieldset(
|
||||||
Legend("Presenter", cls="fieldset-legend"),
|
Legend("Presenter", cls="fieldset-legend"),
|
||||||
Input(type="text",
|
Input(type="text",
|
||||||
name="presenter",
|
name="columns",
|
||||||
value=component.properties.get("filter", ""),
|
value=component.properties.get("columns", ""),
|
||||||
placeholder="Enter filter expression",
|
placeholder="Columns to display, separated by comma",
|
||||||
cls="input w-full"),
|
cls="input w-full"),
|
||||||
P("Comma separated list of columns to display. Use * to display all columns, source=dest to rename columns."),
|
P("Comma separated list of columns to display. Use * to display all columns, source=dest to rename columns."),
|
||||||
cls="fieldset bg-base-200 border-base-300 rounded-box border p-4"
|
cls="fieldset bg-base-200 border-base-300 rounded-box border p-4"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import pandas as pd
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.datagrid_new.components.DataGrid import DataGrid
|
from components.datagrid_new.components.DataGrid import DataGrid
|
||||||
from components.datagrid_new.settings import DataGridSettings
|
from components.datagrid_new.settings import DataGridSettings
|
||||||
from components.workflows.commands import WorkflowPlayerCommandManager
|
from components.workflows.commands import WorkflowPlayerCommandManager
|
||||||
from components.workflows.constants import WORKFLOW_PLAYER_INSTANCE_ID
|
from components.workflows.constants import WORKFLOW_PLAYER_INSTANCE_ID, ProcessorTypes
|
||||||
from components.workflows.db_management import WorkflowsPlayerSettings
|
from components.workflows.db_management import WorkflowsPlayerSettings
|
||||||
from core.utils import get_unique_id, make_safe_id
|
from core.utils import get_unique_id, make_safe_id
|
||||||
|
from workflow.engine import WorkflowEngine, TableDataProducer, DefaultDataPresenter
|
||||||
|
|
||||||
grid_settings = DataGridSettings(
|
grid_settings = DataGridSettings(
|
||||||
header_visible=True,
|
header_visible=True,
|
||||||
@@ -36,6 +38,20 @@ class WorkflowPlayer(BaseComponent):
|
|||||||
grid_settings=grid_settings,
|
grid_settings=grid_settings,
|
||||||
boundaries=boundaries)
|
boundaries=boundaries)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
engine = WorkflowEngine()
|
||||||
|
for component in self._player_settings.components:
|
||||||
|
if component.type == ProcessorTypes.Producer and component.properties["processor_name"] == "Repository":
|
||||||
|
engine.add_processor(TableDataProducer(self._session, self._settings_manager, component.properties["repository"], component.properties["table"]))
|
||||||
|
elif component.type == ProcessorTypes.Presenter and component.properties["processor_name"] == "Default":
|
||||||
|
engine.add_processor(DefaultDataPresenter(component.properties["columns"]))
|
||||||
|
|
||||||
|
res = engine.run_to_list()
|
||||||
|
|
||||||
|
data = [row.as_dict() for row in res]
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
self._datagrid.init_from_dataframe(df)
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
return Div(
|
return Div(
|
||||||
self._datagrid,
|
self._datagrid,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class WorkflowsDesignerState:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class WorkflowsPlayerSettings:
|
class WorkflowsPlayerSettings:
|
||||||
workflow_name: str = "No Name"
|
workflow_name: str = "No Name"
|
||||||
|
components: list[WorkflowComponent] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
70
src/core/Expando.py
Normal file
70
src/core/Expando.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
class Expando:
|
||||||
|
"""
|
||||||
|
Readonly dynamic class that eases the access to attributes and sub attributes
|
||||||
|
It is initialized with a dict
|
||||||
|
You can then access the property using dot '.' (ex. obj.prop1.prop2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, props):
|
||||||
|
self._props = props
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item not in self._props:
|
||||||
|
raise AttributeError(item)
|
||||||
|
|
||||||
|
current = self._props[item]
|
||||||
|
return Expando(current) if isinstance(current, dict) else current
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._props[key] = value
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
"""
|
||||||
|
returns the value, from a string with represents the path
|
||||||
|
:param path:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
current = self._props
|
||||||
|
for attr in path.split("."):
|
||||||
|
if isinstance(current, list):
|
||||||
|
temp = []
|
||||||
|
for value in current:
|
||||||
|
if value and attr in value:
|
||||||
|
temp.append(value[attr])
|
||||||
|
current = temp
|
||||||
|
|
||||||
|
else:
|
||||||
|
if current is None or attr not in current:
|
||||||
|
return None
|
||||||
|
current = current[attr]
|
||||||
|
|
||||||
|
return current
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""
|
||||||
|
Return the information as a dictionary
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self._props.copy()
|
||||||
|
|
||||||
|
def to_dict(self, mappings: dict) -> dict:
|
||||||
|
return {prop_name: self.get(path) for path, prop_name in mappings.items() if prop_name is not None}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if "key" in self._props:
|
||||||
|
return f"Expando(key={self._props["key"]})"
|
||||||
|
|
||||||
|
props_as_str = str(self._props)
|
||||||
|
if len(props_as_str) > 50:
|
||||||
|
props_as_str = props_as_str[:50] + "..."
|
||||||
|
|
||||||
|
return f"Expando({props_as_str})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Expando):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._props == other._props
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(sorted(self._props.items())))
|
||||||
@@ -87,7 +87,7 @@ class MemoryDbEngine:
|
|||||||
obj.update(items)
|
obj.update(items)
|
||||||
|
|
||||||
def exists(self, user_id: str, entry: str):
|
def exists(self, user_id: str, entry: str):
|
||||||
return user_id in entry and entry in self.db[user_id]
|
return user_id in self.db and entry in self.db[user_id]
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from dataclasses import is_dataclass
|
from dataclasses import is_dataclass
|
||||||
|
|
||||||
from components.datagrid_new.db_management import DataGridDbManager
|
from components.datagrid_new.db_management import DataGridDbManager
|
||||||
|
from core.Expando import Expando
|
||||||
|
|
||||||
|
|
||||||
class DataHelper:
|
class DataHelper:
|
||||||
@@ -16,6 +17,8 @@ class DataHelper:
|
|||||||
if object_type:
|
if object_type:
|
||||||
if is_dataclass(object_type):
|
if is_dataclass(object_type):
|
||||||
return [object_type(**row) for row in dataframe.to_dict(orient="records")]
|
return [object_type(**row) for row in dataframe.to_dict(orient="records")]
|
||||||
|
elif object_type is Expando:
|
||||||
|
return [Expando(row) for row in dataframe.to_dict(orient="records")]
|
||||||
else:
|
else:
|
||||||
raise ValueError("object_type must be a dataclass type")
|
raise ValueError("object_type must be a dataclass type")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Generator
|
from typing import Any, Generator
|
||||||
|
|
||||||
|
from core.Expando import Expando
|
||||||
|
from utils.Datahelper import DataHelper
|
||||||
|
|
||||||
|
|
||||||
class DataProcessor(ABC):
|
class DataProcessor(ABC):
|
||||||
"""Base class for all data processing components."""
|
"""Base class for all data processing components."""
|
||||||
@@ -47,6 +50,45 @@ class DataPresenter(DataProcessor):
|
|||||||
yield self.present(data)
|
yield self.present(data)
|
||||||
|
|
||||||
|
|
||||||
|
class TableDataProducer(DataProducer):
|
||||||
|
"""Base class for data producers that emit data from a repository."""
|
||||||
|
|
||||||
|
def __init__(self, session, settings_manager, repository_name, table_name):
|
||||||
|
self._session = session
|
||||||
|
self.settings_manager = settings_manager
|
||||||
|
self.repository_name = repository_name
|
||||||
|
self.table_name = table_name
|
||||||
|
|
||||||
|
def emit(self, data: Any = None) -> Generator[Any, None, None]:
|
||||||
|
yield from DataHelper.get(self._session, self.settings_manager, self.repository_name, self.table_name, Expando)
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultDataPresenter(DataPresenter):
|
||||||
|
"""Default data presenter that returns the input data unchanged."""
|
||||||
|
|
||||||
|
def __init__(self, columns_as_str: str):
|
||||||
|
super().__init__()
|
||||||
|
if not columns_as_str or columns_as_str == "*":
|
||||||
|
self.mappings = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
self.mappings = {}
|
||||||
|
temp_mappings = [col.strip() for col in columns_as_str.split(",")]
|
||||||
|
for mapping in temp_mappings:
|
||||||
|
if "=" in mapping:
|
||||||
|
key, value = mapping.split("=")
|
||||||
|
self.mappings[key] = value
|
||||||
|
else:
|
||||||
|
self.mappings[mapping] = mapping
|
||||||
|
|
||||||
|
def present(self, data: Any) -> Any:
|
||||||
|
if self.mappings is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
return Expando(data.to_dict(self.mappings))
|
||||||
|
|
||||||
|
|
||||||
class WorkflowEngine:
|
class WorkflowEngine:
|
||||||
"""Orchestrates the data processing pipeline using generators."""
|
"""Orchestrates the data processing pipeline using generators."""
|
||||||
|
|
||||||
@@ -93,4 +135,3 @@ class WorkflowEngine:
|
|||||||
Use this method when you need all results at once.
|
Use this method when you need all results at once.
|
||||||
"""
|
"""
|
||||||
return list(self.run())
|
return list(self.run())
|
||||||
|
|
||||||
|
|||||||
66
tests/test_data_helper.py
Normal file
66
tests/test_data_helper.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from components.datagrid_new.components.DataGrid import DataGrid
|
||||||
|
from core.Expando import Expando
|
||||||
|
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||||
|
from utils.Datahelper import DataHelper
|
||||||
|
|
||||||
|
TEST_GRID_ID = "testing_grid_id"
|
||||||
|
TEST_GRID_KEY = ("RepoName", "TableName")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def settings_manager():
|
||||||
|
return SettingsManager(MemoryDbEngine())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def datagrid(session, settings_manager):
|
||||||
|
dg = DataGrid(session,
|
||||||
|
_id=TEST_GRID_ID,
|
||||||
|
settings_manager=settings_manager,
|
||||||
|
key=TEST_GRID_KEY,
|
||||||
|
boundaries={"height": 500, "width": 800})
|
||||||
|
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'Name': ['Alice', 'Bob'],
|
||||||
|
'Age': [20, 25],
|
||||||
|
'Is Student': [True, False],
|
||||||
|
})
|
||||||
|
|
||||||
|
dg.init_from_dataframe(df, save_state=True)
|
||||||
|
return dg
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get_data_as_dataframe(session, settings_manager, datagrid):
|
||||||
|
res = DataHelper.get(session, settings_manager, "RepoName", "TableName")
|
||||||
|
assert isinstance(res, pd.DataFrame)
|
||||||
|
assert res.equals(datagrid.get_dataframe())
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get_data_as_dataclass(session, settings_manager, datagrid):
|
||||||
|
@dataclass
|
||||||
|
class DataclassTestClass:
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
is_student: bool
|
||||||
|
|
||||||
|
res = DataHelper.get(session, settings_manager, "RepoName", "TableName", DataclassTestClass)
|
||||||
|
assert isinstance(res, list)
|
||||||
|
assert res == [
|
||||||
|
DataclassTestClass("Alice", 20, True),
|
||||||
|
DataclassTestClass("Bob", 25, False),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get_data_as_expando(session, settings_manager, datagrid):
|
||||||
|
res = DataHelper.get(session, settings_manager, "RepoName", "TableName", Expando)
|
||||||
|
assert isinstance(res, list)
|
||||||
|
assert res == [
|
||||||
|
Expando({"name": "Alice", "age": 20, "is_student": True}),
|
||||||
|
Expando({"name": "Bob", "age": 25, "is_student": False})
|
||||||
|
]
|
||||||
|
|
||||||
75
tests/test_expando.py
Normal file
75
tests/test_expando.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.Expando import Expando
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get_properties():
|
||||||
|
props = {"a": 10,
|
||||||
|
"b": {
|
||||||
|
"c": "value",
|
||||||
|
"d": 20
|
||||||
|
}}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.a == 10
|
||||||
|
assert dynamic.b.c == "value"
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
assert dynamic.unknown == "some_value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get():
|
||||||
|
props = {"a": 10,
|
||||||
|
"b": {
|
||||||
|
"c": "value",
|
||||||
|
"d": 20
|
||||||
|
}}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.get("a") == 10
|
||||||
|
assert dynamic.get("b.c") == "value"
|
||||||
|
assert dynamic.get("unknown") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_get_from_list():
|
||||||
|
props = {"a": [{"c": "value1", "d": 1}, {"c": "value2", "d": 2}]}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.get("a.c") == ["value1", "value2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_none_is_returned_when_get_from_list_and_property_does_not_exist():
|
||||||
|
props = {"a": [{"c": "value1", "d": 1},
|
||||||
|
{"a": "value2", "d": 2} # 'c' does not exist in the second row
|
||||||
|
]}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.get("a.c") == ["value1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_none_values():
|
||||||
|
props = {"a": 10,
|
||||||
|
"b": None}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.get("b.c") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_none_values_in_list():
|
||||||
|
props = {"a": [{"b": {"c": "value"}},
|
||||||
|
{"b": None}
|
||||||
|
]}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
|
||||||
|
assert dynamic.get("a.b.c") == ["value"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_add_new_properties():
|
||||||
|
props = {"a": 10,
|
||||||
|
"b": 20}
|
||||||
|
dynamic = Expando(props)
|
||||||
|
dynamic["c"] = 30
|
||||||
|
|
||||||
|
assert dynamic.a == 10
|
||||||
|
assert dynamic.b == 20
|
||||||
|
assert dynamic.c == 30
|
||||||
Reference in New Issue
Block a user