Skip to content

Commit a959096

Browse files
start report refactoring #219
1 parent 7698fb7 commit a959096

File tree

9 files changed

+367
-0
lines changed

9 files changed

+367
-0
lines changed

src/feelpp/benchmarking/dashboardRenderer/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from feelpp.benchmarking.dashboardRenderer.schemas.dashboardSchema import ComponentMetadata
2+
3+
class Component:
4+
def __init__(self, id, component_metadata: ComponentMetadata):
5+
self.id = id
6+
self.display_name = component_metadata.display_name
7+
self.description = component_metadata.description
8+
9+
self.views = {}
10+
11+
def __repr__(self):
12+
return f"<{self.id}>"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from feelpp.benchmarking.report.renderer import Renderer
2+
from repository import ComponentRepository
3+
from feelpp.benchmarking.dashboardRenderer.utils import TreeUtils
4+
5+
6+
class DashboardRenderer:
7+
""" Serves as a repository orchestrator"""
8+
def __init__(self,components_config,export_base_path):
9+
10+
self.component_repositories = [
11+
ComponentRepository(repository_id, components)
12+
for repository_id, components in components_config.components.items()
13+
]
14+
15+
self.initRepositoryViews(components_config.views,components_config.component_map)
16+
17+
def initRepositoryViews(self,views,component_map):
18+
tree_order = component_map.component_order
19+
mapping = component_map.mapping
20+
for component_repository in self.component_repositories:
21+
component_views = views[component_repository.id]
22+
view_orders = TreeUtils.treeToLists({component_repository.id:component_views})
23+
for view_order in view_orders:
24+
view_perm = [tree_order.index(v) for v in view_order]
25+
permuted_tree = TreeUtils.permuteTreeLevels(mapping,view_perm)
26+
component_repository.initViews(view_order,permuted_tree,self.component_repositories)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
from feelpp.benchmarking.dashboardRenderer.component import Component
3+
4+
class Repository:
5+
""" Base class for repositories.
6+
Designed for containing and manipulating a unique list of items
7+
"""
8+
def __init__(self,id):
9+
self.data = []
10+
self.id = id
11+
12+
def __iter__(self):
13+
""" Iterator for the repository """
14+
return iter(self.data)
15+
16+
def __repr__(self):
17+
return f"< {self.id} : [{', '.join([str(d) for d in self.data])}] >"
18+
19+
def add(self, item):
20+
""" Add an item to the repository, ensuring it is unique
21+
Args:
22+
item (object): The item to add
23+
"""
24+
if item not in self.data and item.id not in [x.id for x in self.data]:
25+
self.data.append(item)
26+
27+
def get(self, id):
28+
""" Get an item by its id """
29+
return next(filter(lambda x: x.id == id, self.data))
30+
31+
def __getitem__(self,index):
32+
return self.data[index]
33+
34+
35+
class ComponentRepository(Repository):
36+
"""Class representing a collection of components (machines, applications, useCases) """
37+
def __init__(self, id, components):
38+
super().__init__(id)
39+
for component_id, component_metadata in components.items():
40+
self.add(Component(component_id, component_metadata))
41+
42+
def initViews(self,view_order,tree, other_repositories):
43+
repo_lookup = {rep.id : rep for rep in other_repositories}
44+
45+
def processViewTree(subtree, level_index):
46+
if level_index >= len(view_order) or not subtree:
47+
return {}
48+
49+
current_repo = repo_lookup[view_order[level_index]]
50+
view_structure = {}
51+
52+
for view_component_id, sub_tree in subtree.items():
53+
view_component = current_repo.get(view_component_id)
54+
view_structure[view_component] = processViewTree(sub_tree, level_index + 1)
55+
56+
return view_structure
57+
58+
for component in self.data:
59+
component_subtree = tree.get(component.id, {})
60+
component.views[view_order[1]] = processViewTree(component_subtree, 1)
61+
62+
# def renderSelf(self, base_dir, renderer, self_tag_id, parent_id = "catalog-index"):
63+
# """ Initialize the module for repository.
64+
# Creates the directory for the repository and renders the index.adoc file
65+
# Args:
66+
# base_dir (str): The base directory for the modules
67+
# renderer (Renderer): The renderer to use
68+
# self_tag_id (str): The catalog id of the current reposirory, to be used by their children as parent
69+
# parent_id (str): The catalog id of the parent component
70+
# """
71+
# module_path = os.path.join(base_dir, self.id)
72+
73+
# if not os.path.exists(module_path):
74+
# os.mkdir(module_path)
75+
76+
# renderer.render(
77+
# os.path.join(module_path,"index.adoc"),
78+
# self.indexData(parent_id,self_tag_id)
79+
# )
80+
81+
# def renderChildren(self, base_dir, renderer):
82+
# """ Inits the repository module and calls the initModules method of each item in the repository.
83+
# Args:
84+
# base_dir (str): The base directory for the modules
85+
# renderer (Renderer): The renderer to use
86+
# parent_id (str,optional): The catalog id of the parent component. Defaults to "supercomputers".
87+
# """
88+
# for item in self.data:
89+
# item.render(os.path.join(base_dir,self.id), renderer, self.id)
90+
91+
# def render(self, base_dir, renderer, parent_id = "catalog-index"):
92+
# """ Inits the repository module and calls the initModules method of each item in the repository.
93+
# Args:
94+
# base_dir (str): The base directory for the modules
95+
# renderer (Renderer): The renderer to use
96+
# parent_id (str,optional): The catalog id of the parent component. Defaults to "supercomputers".
97+
# """
98+
# self.renderSelf(base_dir,renderer,self_tag_id=self.id, parent_id=parent_id)
99+
# self.renderChildren(base_dir, renderer)

src/feelpp/benchmarking/dashboardRenderer/schemas/__init__.py

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pydantic import BaseModel
2+
from typing import List, Dict, Optional
3+
4+
class ComponentMap(BaseModel):
5+
component_order: List[str]
6+
mapping: Dict[str,Dict]
7+
8+
class ComponentMetadata(BaseModel):
9+
display_name: str
10+
description: Optional[str] = ""
11+
12+
class DashboardSchema(BaseModel):
13+
component_map: ComponentMap
14+
components: Dict[str,Dict[str, ComponentMetadata]]
15+
views : Dict[str,Dict]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import pytest
2+
from feelpp.benchmarking.dashboardRenderer.schemas.dashboardSchema import ComponentMetadata
3+
from feelpp.benchmarking.dashboardRenderer.repository import ComponentRepository
4+
from feelpp.benchmarking.dashboardRenderer.component import Component
5+
from feelpp.benchmarking.dashboardRenderer.utils import TreeUtils
6+
7+
class TestComponentRepository:
8+
9+
@staticmethod
10+
def checkForDuplicates(list):
11+
assert len(list) == len(set(list)), "list has duplicate values"
12+
13+
def test_uniqueness(self):
14+
components_config = {
15+
"a": ComponentMetadata(display_name = "A"),
16+
"b": ComponentMetadata(display_name = "B"),
17+
"c": ComponentMetadata(display_name = "C"),
18+
"a": ComponentMetadata(display_name = "A"),
19+
}
20+
component_repository = ComponentRepository("test",components_config)
21+
self.checkForDuplicates(component_repository.data)
22+
assert len([c for c in component_repository.data if c.id == "a"]) == 1
23+
24+
component_repository.add(Component(id = "b",component_metadata=ComponentMetadata(display_name="Other")))
25+
self.checkForDuplicates(component_repository.data)
26+
assert len([c for c in component_repository.data if c.id == "b"]) == 1
27+
28+
29+
def test_initViews(self):
30+
"""Test the initViews method with different view order permutations."""
31+
32+
# --- Create Components in each repository ---
33+
components_config_1 = {"A1": ComponentMetadata(display_name="A1"), "A2": ComponentMetadata(display_name="A2")}
34+
components_config_2 = {"B1": ComponentMetadata(display_name="B1"), "B2": ComponentMetadata(display_name="B2")}
35+
components_config_3 = {"C1": ComponentMetadata(display_name="C1"), "C2": ComponentMetadata(display_name="C2")}
36+
components_config_4 = {"D1": ComponentMetadata(display_name="D1"), "D2": ComponentMetadata(display_name="D2")}
37+
38+
# --- Create Repositories ---
39+
repo1 = ComponentRepository("Repo1", components_config_1)
40+
repo2 = ComponentRepository("Repo2", components_config_2)
41+
repo3 = ComponentRepository("Repo3", components_config_3)
42+
repo4 = ComponentRepository("Repo4", components_config_4)
43+
44+
# Get the actual components from repositories
45+
component_A1 = repo1.get("A1")
46+
component_A2 = repo1.get("A2")
47+
component_B1 = repo2.get("B1")
48+
component_B2 = repo2.get("B2")
49+
component_C1 = repo3.get("C1")
50+
component_C2 = repo3.get("C2")
51+
component_D1 = repo4.get("D1")
52+
component_D2 = repo4.get("D2")
53+
54+
repositories = [repo1, repo2, repo3, repo4]
55+
view_order = ["Repo1", "Repo2", "Repo3", "Repo4"]
56+
# --- Define a Hierarchical Tree ---
57+
tree = {
58+
"A1": {
59+
"B1": {
60+
"C1": {
61+
"D1": {}
62+
},
63+
"C2": {
64+
"D2": {}
65+
}
66+
},
67+
"B2": {
68+
"C1": {
69+
"D2": {}
70+
}
71+
}
72+
},
73+
"A2": {
74+
"B1": {
75+
"C2": {
76+
"D1": {}
77+
}
78+
}
79+
}
80+
}
81+
82+
# --- Run initViews on repo1 ---
83+
repo1.initViews(view_order, tree, repositories)
84+
85+
# Ensure view key exists in components
86+
assert view_order[1] in component_A1.views
87+
assert view_order[1] in component_A2.views
88+
89+
# Expected structure after `initViews`
90+
expected_structure_A1 = {
91+
component_B1: {
92+
component_C1: {component_D1: {}},
93+
component_C2: {component_D2: {}}
94+
},
95+
component_B2: {
96+
component_C1: {component_D2: {}}
97+
}
98+
}
99+
100+
expected_structure_A2 = {
101+
component_B1: {
102+
component_C2: {component_D1: {}}
103+
}
104+
}
105+
106+
assert component_A1.views[view_order[1]] == expected_structure_A1, (
107+
f"Expected {expected_structure_A1}, but got {component_A1.views[view_order[1]]}"
108+
)
109+
110+
assert component_A2.views[view_order[1]] == expected_structure_A2, (
111+
f"Expected {expected_structure_A2}, but got {component_A2.views[view_order[1]]}"
112+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from feelpp.benchmarking.dashboardRenderer.utils import TreeUtils
2+
import pytest
3+
4+
@pytest.mark.parametrize("tree, expected", [
5+
({}, []), # Empty tree case
6+
({"A": "B"}, [["A", "B"]]), # Single key-value pair
7+
({"A": {"B": "C"}}, [["A", "B", "C"]]), # Simple nested case
8+
({"A": {"B": "C", "D": "E"}}, [["A", "B", "C"], ["A", "D", "E"]]), # Two branches at second level
9+
({"A": {"B": {"C": "D"}}}, [["A", "B", "C", "D"]]), # Deeply nested single path
10+
({"A": {"B": "C"}, "D": {"E": "F"}}, [["A", "B", "C"], ["D", "E", "F"]]), # Multiple top-level keys
11+
({"A": {"B": {"C": "D", "E": "F"}, "G": "H"}},
12+
[["A", "B", "C", "D"], ["A", "B", "E", "F"], ["A", "G", "H"]]) # Complex case with mixed depths
13+
])
14+
def test_treeToList(tree,expected):
15+
tree_list = TreeUtils.treeToLists(tree)
16+
assert tree_list == expected
17+
18+
19+
20+
@pytest.mark.parametrize("tree, permutation, expected", [
21+
({"A": {"B": "C"}}, [0, 1], {"A": {"B": "C"}}), # Basic case: No permutation (identity)
22+
({"A": {"B": "C"}}, [1, 0], {"B": {"A": "C"}}), # Swap two levels
23+
({"A": {"B": {"C": "D"}}}, [1, 0, 2], {"B": {"A": {"C": "D"}}}), # Three levels, swapping first and second
24+
({"A": {"B": {"C": "D", "E": "F"}}}, [1, 0, 2], {"B": {"A": {"C": "D", "E": "F"}}}), # More complex tree with swaps
25+
({"A": {"B": {"C": "D"}}}, [2, 1, 0], {"C": {"B": {"A": "D"}}}), # Three levels, swapping first and third
26+
({
27+
"A": {"B": {"C": "D", "E": "F"}},
28+
"X": {"Y": {"Z": "W"}}
29+
}, [1, 0, 2],
30+
{
31+
"B": {"A": {"C": "D", "E": "F"}},
32+
"Y": {"X": {"Z": "W"}}
33+
}) # Multiple branches, swapping levels
34+
])
35+
def test_permuteTreeLevels(tree, permutation, expected):
36+
"""Test that permuteTreeLevels correctly rearranges tree levels."""
37+
result = TreeUtils.permuteTreeLevels(tree, permutation)
38+
assert result == expected
39+
40+
@pytest.mark.parametrize(("tree","permutation", "error"), [
41+
({"A": {"B": "C"}}, [0], ValueError), # Invalid permutation length
42+
({"A": {"B": {"C": "D"}}}, [0, 1], ValueError), # Missing level in permutation
43+
({"A": {"B": "C"}}, [2, 1], IndexError), # Out of range permutation
44+
])
45+
def test_permuteTreeLevels_errors(tree, permutation, error):
46+
"""Test that permuteTreeLevels raises error on incorrect permutations."""
47+
with pytest.raises(error):
48+
TreeUtils.permuteTreeLevels(tree, permutation)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
class TreeUtils:
2+
3+
@staticmethod
4+
def treeToLists(tree, prefix=[]):
5+
tree_list = []
6+
for k, v in tree.items():
7+
new_prefix = prefix + [k]
8+
if isinstance(v, dict):
9+
tree_list.extend(TreeUtils.treeToLists(v, new_prefix))
10+
else:
11+
tree_list.append(new_prefix + [v])
12+
return tree_list
13+
14+
15+
@staticmethod
16+
def permuteTreeLevels(tree, permutation):
17+
"""Permute the levels of an N-level nested dictionary tree."""
18+
branches = []
19+
20+
def traverse(subtree, path):
21+
if isinstance(subtree, dict) and subtree:
22+
for key, child in subtree.items():
23+
traverse(child, path + [key])
24+
else:
25+
branches.append((path, subtree))
26+
27+
traverse(tree, [])
28+
29+
N = len(permutation)
30+
31+
for path, leaf in branches:
32+
if len(path) != N:
33+
raise ValueError(f"Branch {path} has {len(path)} levels but expected {N}.")
34+
35+
new_branches = []
36+
for path, leaf in branches:
37+
new_path = [None] * N
38+
for new_level, old_level in enumerate(permutation):
39+
new_path[new_level] = path[old_level]
40+
new_branches.append((new_path, leaf))
41+
42+
new_tree = {}
43+
for path, leaf in new_branches:
44+
current = new_tree
45+
for key in path[:-1]:
46+
if key not in current:
47+
current[key] = {}
48+
49+
elif not isinstance(current[key], dict):
50+
raise ValueError(f"Conflict encountered at key '{key}' while rebuilding the tree.")
51+
current = current[key]
52+
53+
current[path[-1]] = leaf
54+
55+
return new_tree

0 commit comments

Comments
 (0)