Source code for nwm_region_mgr.utils.string_utils
"""Define utilities and/or help functions for string manipulation.
string_utils.py
Functions:
- expand_with_vpu: Expand a string with {vpu_list} placeholders using a list of VPU codes from the context.
- recursive_substitute: Recursively substitute placeholders in a Pydantic model, dictionary, or string.
"""
from itertools import product
from typing import Any
from pydantic import BaseModel
[docs]
def expand_with_lists(template_str: str, context: dict) -> dict | str:
"""Expand a string with list placeholders using Cartesian product.
Always returns a dict if any list placeholders are involved — even if only one result.
Otherwise, returns a plain substituted string.
"""
# Find all list-type keys in context used in the string
list_keys = [
k
for k in context
if isinstance(context[k], list) and f"{{{k}}}" in template_str
]
if not list_keys:
# No list placeholders → do a simple substitution
return template_str.format(**context)
# Get all combinations of list values
combinations = list(product(*[context[k] for k in list_keys]))
result = {}
for combo in combinations:
sub_context = context.copy()
sub_context.update(dict(zip(list_keys, combo)))
key = "_".join(str(v) for v in combo)
result[key] = template_str.format(**sub_context)
return result
[docs]
def recursive_substitute(obj: Any, context: dict) -> Any:
"""Recursively substitute placeholders in nested structures."""
if isinstance(obj, BaseModel):
data = obj.model_dump()
substituted = recursive_substitute(data, context)
return obj.__class__(**substituted)
elif isinstance(obj, dict):
return {k: recursive_substitute(v, context) for k, v in obj.items()}
elif isinstance(obj, str):
try:
return expand_with_lists(obj, context)
except KeyError:
return obj # leave unchanged if substitution fails
else:
return obj
[docs]
def recursive_substitute_multi_lists(obj: Any, context: dict) -> Any:
"""Recursively substitute placeholders, handling multiple lists with Cartesian expansion."""
if isinstance(obj, BaseModel):
data = obj.model_dump()
substituted = recursive_substitute_multi_lists(data, context)
return obj.__class__(**substituted)
elif isinstance(obj, dict):
return {k: recursive_substitute_multi_lists(v, context) for k, v in obj.items()}
elif isinstance(obj, str):
try:
# Identify list-type variables that appear in the string
list_keys = [
k
for k, v in context.items()
if isinstance(v, list) and f"{{{k}}}" in obj
]
if not list_keys:
return obj.format(**context)
# Create combinations of list values
list_values = [context[k] for k in list_keys]
print(f"List keys: {list_keys}, List values: {list_values}")
combinations = list(product(*list_values))
print(f"Combinations: {combinations}")
# Generate expanded strings for each combination
results = []
for combo in combinations:
print(f"Processing combination: {combo}")
print(combo)
temp_context = context.copy()
temp_context.update(dict(zip(list_keys, combo)))
results.append(obj.format(**temp_context))
return results
except KeyError:
return obj # Leave unchanged if context is incomplete
else:
return obj