Files

252 lines
9.4 KiB
Python

#!/usr/bin/env python3
##############################################################################
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
##############################################################################
"""
Apply delta YAML to base architecture to produce target architecture.
Usage: python apply_config_deltas.py <base_arch_dir> <delta_yaml> <output_dir>
"""
from __future__ import annotations
import shutil
import sys
from pathlib import Path
from typing import Any, Optional, Union
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from config_management import utils_ruamel as cm_utils # noqa: E402
def find_table(config: dict, table_id: Any) -> Optional[dict]:
"""Find and return the table with given id, or None."""
for item in config.get("Panel Config", {}).get("data source", []):
table = item.get("metric_table")
if isinstance(table, dict) and table.get("id") == table_id:
return table
return None
def add_table(config: dict, metric_table: dict) -> None:
"""Add entire new table to config."""
config.setdefault("Panel Config", {}).setdefault("data source", []).append({
"metric_table": metric_table
})
print(f"Added table: {metric_table.get('id')} - {metric_table.get('title')}")
def add_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
"""Add metrics to existing table."""
table = find_table(config, table_id)
if table is None:
print(f"WARNING: Table {table_id} not found for metric addition")
return
table.setdefault("metric", {})
for metric_dict in metrics:
for metric_name, metric_data in metric_dict.items():
table["metric"][metric_name] = metric_data
print(f"Added metric: {metric_name} to table {table_id}")
def delete_table(config: dict, table_id: Any) -> None:
"""Remove entire table from config."""
data_source = config.get("Panel Config", {}).get("data source", [])
for idx, item in enumerate(list(data_source)):
table = item.get("metric_table")
if isinstance(table, dict) and table.get("id") == table_id:
del data_source[idx]
print(f"Deleted table: {table_id}")
return
print(f"WARNING: Table {table_id} not found for deletion")
def delete_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
"""Remove specific metrics from table."""
table = find_table(config, table_id)
if table is None or "metric" not in table:
print(f"WARNING: Table {table_id} not found or has no metrics")
return
for metric_dict in metrics:
for metric_name in metric_dict.keys():
if metric_name in table["metric"]:
del table["metric"][metric_name]
print(f"Deleted metric: {metric_name} from table {table_id}")
def modify_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
"""Modify specific fields in existing metrics."""
table = find_table(config, table_id)
if table is None or "metric" not in table:
print(f"WARNING: Table {table_id} not found or has no metrics")
return
for metric_dict in metrics:
for metric_name, new_fields in metric_dict.items():
if metric_name not in table["metric"]:
print(f"WARNING: Metric '{metric_name}' not found in table {table_id}")
continue
for field_name, field_value in new_fields.items():
table["metric"][metric_name][field_name] = field_value
print(f"Modified {metric_name}.{field_name} in table {table_id}")
def add_descriptions(config: dict, descriptions: dict) -> None:
"""Add metric descriptions to config."""
md = config["Panel Config"].setdefault("metrics_description", {})
for metric_name, desc_data in descriptions.items():
md[metric_name] = dict(desc_data) if isinstance(desc_data, dict) else desc_data
print(f"Added description: {metric_name}")
def delete_descriptions(config: dict, descriptions: dict) -> None:
"""Remove metric descriptions from config."""
md = config["Panel Config"].setdefault("metrics_description", {})
for metric_name in descriptions.keys():
if metric_name in md:
del md[metric_name]
print(f"Deleted description: {metric_name}")
def modify_descriptions(config: dict, descriptions: dict) -> None:
"""Modify metric descriptions in config."""
md = config["Panel Config"].setdefault("metrics_description", {})
for metric_name, desc_data in descriptions.items():
if isinstance(desc_data, dict):
new_dict = {}
for k, v in desc_data.items():
new_dict[k] = v
md[metric_name] = new_dict
else:
md[metric_name] = desc_data
print(f"Added description: {metric_name}")
def apply_changes(config: dict, changes: list[dict], category: str) -> None:
"""Apply delta changes to configuration."""
for change in changes:
mt = change.get("metric_table")
if mt:
table_id = mt.get("id")
if category == "Addition":
if "metrics" in mt:
add_metrics(config, table_id, mt["metrics"])
elif "metric" in mt:
add_table(config, mt)
elif category == "Deletion":
if "metrics" in mt:
delete_metrics(config, table_id, mt["metrics"])
else:
delete_table(config, table_id)
elif category == "Modification":
if "metrics" in mt:
modify_metrics(config, table_id, mt["metrics"])
descriptions = change.get("metric_descriptions", {})
if descriptions:
if category == "Addition":
add_descriptions(config, descriptions)
elif category == "Deletion":
delete_descriptions(config, descriptions)
elif category == "Modification":
modify_descriptions(config, descriptions)
def apply_delta(
base_dir: Union[str, Path],
delta_file: Union[str, Path],
output_dir: Union[str, Path],
) -> None:
"""Apply delta YAML to all files in base directory."""
delta = cm_utils.load_yaml(delta_file, round_trip=True)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
changes_by_panel: dict[Any, dict[str, list[dict]]] = {}
for category in ("Addition", "Deletion", "Modification"):
for change in delta.get(category, []):
panel_id = change.get("Panel Config", {}).get("id")
panel_bucket = changes_by_panel.setdefault(
panel_id, {"Addition": [], "Deletion": [], "Modification": []}
)
panel_bucket[category].append(change)
base_path = Path(base_dir)
for yaml_file in base_path.glob("*.yaml"):
config = cm_utils.load_yaml(yaml_file, round_trip=True)
panel_id = config.get("Panel Config", {}).get("id")
if panel_id in changes_by_panel:
print(f"\nApplying deltas to {yaml_file.name} (Panel ID: {panel_id})")
# Process in safe order: deletions -> modifications -> additions
for category in ("Deletion", "Modification", "Addition"):
if changes_by_panel[panel_id][category]:
apply_changes(
config, changes_by_panel[panel_id][category], category
)
cm_utils.strip_existing_header(config)
cm_utils.save_yaml(config, output_path / yaml_file.name)
print(f"Saved: {yaml_file.name}")
else:
shutil.copy(yaml_file, output_path / yaml_file.name)
def main() -> None:
if len(sys.argv) != 4:
print(
"Usage: python apply_config_deltas.py "
"<base_arch_dir> <delta_yaml> <output_dir>"
)
sys.exit(1)
base_dir, delta_file, output_dir = sys.argv[1:4]
if not Path(base_dir).is_dir():
print(f"Error: {base_dir} is not a directory")
sys.exit(1)
if not Path(delta_file).is_file():
print(f"Error: {delta_file} is not a file")
sys.exit(1)
apply_delta(base_dir, delta_file, output_dir)
print("\nDelta application complete!")
if __name__ == "__main__":
main()