From ace0062f379154cd2c3e84d982340aaf79a48092 Mon Sep 17 00:00:00 2001 From: Linus Vogel Date: Sun, 15 Feb 2026 11:13:22 +0100 Subject: [PATCH] implemented utility function for merging dictionaries --- pillar_tool/routers/pillar.py | 43 +++++++++++++++++++++++ pillar_tool/schemas.py | 6 +++- pillar_tool/util/pillar_utilities.py | 52 ++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 pillar_tool/util/pillar_utilities.py diff --git a/pillar_tool/routers/pillar.py b/pillar_tool/routers/pillar.py index e69de29..4afebc3 100644 --- a/pillar_tool/routers/pillar.py +++ b/pillar_tool/routers/pillar.py @@ -0,0 +1,43 @@ +import uuid + +from fastapi.params import Depends +from sqlalchemy import select, insert, delete +from sqlalchemy.orm import Session +from starlette.exceptions import HTTPException +from starlette.requests import Request +from fastapi import APIRouter +from starlette.responses import JSONResponse + +from pillar_tool.db.models.top_data import State, StateAssignment +from pillar_tool.schemas import PillarParams, get_model_from_query +from pillar_tool.util.validation import validate_state_name + +router = APIRouter( + prefix="/pillar", + tags=["pillar"], +) + +# Note: there is no list of all pillars, as this would not be helpful + +@router.get("/{name}") +def state_get(req: Request, name: str, params: Depends(get_model_from_query(PillarParams))): + # TODO: implement + # this function should: + # - get the affected host hierarchy + # - get all the relevant pillar dictionaries + # - merge the pillar directories + # - return the merged pillar directory + # if any error happens, return non-200 status and an empty dictionary so that salt does not shit itself + db: Session = req.state.db + + +@router.post("/{name}") +def state_create(req: Request, name: str): + # TODO: implement + db = req.state.db + + +@router.delete("/{name}") +def state_delete(req: Request, name: str): + # TODO: implement + db = req.state.db diff --git a/pillar_tool/schemas.py b/pillar_tool/schemas.py index 03e1d86..b39047f 100644 --- a/pillar_tool/schemas.py +++ b/pillar_tool/schemas.py @@ -50,7 +50,11 @@ class HostgroupParams(BaseModel): class StateParams(BaseModel): pass # No parameters needed for state operations currently - +# Pillar operations +class PillarParams(BaseModel): + target: str # must be host or hostgroup + value: str | None # value if the pillar should be set + type: str | None # type of pillar if pillar should be set def get_model_from_query[T](model: T) -> Callable[[Request], T]: diff --git a/pillar_tool/util/pillar_utilities.py b/pillar_tool/util/pillar_utilities.py new file mode 100644 index 0000000..e0e4284 --- /dev/null +++ b/pillar_tool/util/pillar_utilities.py @@ -0,0 +1,52 @@ +from copy import deepcopy + + +def apply_layer(base: dict, layer: dict): + """ + Recursively applies key-value pairs from `layer` onto `base`. + + For each key in `layer`: + - If both `base[key]` and `layer[key]` are dictionaries, recursively merge them. + - Otherwise, `base[key]` is overwritten (or newly inserted) with `layer[key]`. + + Note: This function mutates the `base` dictionary in-place. + + :param base: The target dictionary to be updated. Will be modified directly. + :param layer: The source dictionary whose values will be applied to `base`. + """ + for key, value in layer.items(): + # if base and layer value are dicts, apply recursively + if type(value) is dict and key in base and type(base[key]) is dict: + apply_layer(base[key], value) + # else replace the base value with the layer value + # or insert the base value + else: + base[key] = value + + +def merge(*pillar_data, deep_copy=True) -> dict: + """ + Merges multiple pillar data dictionaries into one. + + The merging is done left-to-right: keys from later dictionaries override + those in earlier ones. Nested dictionaries are merged recursively using + `apply_layer`. + + :param pillar_data: Two or more dictionaries to merge. Must contain at least one item. + :param deep_copy: If True (default), the first dictionary is deep-copied before merging, + preserving the original input data. If False, the first dictionary + is modified in-place. + :return: A new merged dictionary (if `deep_copy=True`) or the mutated first dictionary (if `deep_copy=False`). + + Example: + merge({'a': 1}, {'b': 2}) → {'a': 1, 'b': 2} + merge({'a': {'x': 1}}, {'a': {'y': 2}}) → {'a': {'x': 1, 'y': 2}} + """ + assert len(pillar_data) > 0, "At least one pillar data is required" + merged_pillar = deepcopy(pillar_data[0]) if deep_copy else pillar_data[0] + + for pillar in pillar_data[1:]: + apply_layer(merged_pillar, pillar) + + + return merged_pillar \ No newline at end of file