Source code for hydroflows.workflow.wildcards
"""Workflow wildcards module."""
import itertools
from logging import getLogger
from pathlib import Path
from pydantic import BaseModel
from hydroflows.utils.parsers import get_wildcards
logger = getLogger(__name__)
[docs]
class Wildcards(BaseModel):
"""Wildcards class.
This class is used to define the wildcards for the workflow.
"""
wildcards: dict[str, list[str]] = {}
"""List of wildcard keys and values."""
@property
def names(self) -> list[str]:
"""Get the names of the wildcards."""
return list(self.wildcards.keys())
@property
def values(self) -> list[list]:
"""Get the values of the wildcards."""
return list(self.wildcards.values())
[docs]
def to_dict(self) -> dict[str, list]:
"""Convert the wildcards to a dictionary of names and values."""
return self.model_dump()["wildcards"]
[docs]
def set(self, key: str, values: list[str]):
"""Add a wildcard."""
key = str(key).lower()
if key in self.wildcards:
if values != self.wildcards[key]:
raise KeyError(f"Wildcard '{key}' already exists.")
logger.debug(f"Wildcard '{key}' already exists with identical keys.")
return
if not isinstance(values, list):
raise TypeError("Values must be a list.")
self.wildcards.update({key: values})
logger.info(f"Added wildcard '{key}' with values: {values}")
[docs]
def get(self, key: str) -> list[str]:
"""Get the values of a wildcard."""
key = str(key).lower()
if key not in self.wildcards:
raise KeyError(
f"Wildcard '{key}' not found. "
f"Available wildcards are: {', '.join(self.names)}"
)
return self.wildcards[key]
def wildcard_product(wildcards: dict[str, list[str]]) -> list[dict[str, str]]:
"""Get the product of wildcard values.
Parameters
----------
wildcards : dict[str, list[str]]
The wildcards and values to get the product of.
"""
wildcard_keys = list(wildcards.keys())
return [
dict(zip(wildcard_keys, values))
for values in itertools.product(*[wildcards[wc] for wc in wildcard_keys])
]
def resolve_wildcards(
s: str | Path, wildcards: dict[str, list[str] | str]
) -> list[str | Path] | str | Path:
"""Resolve wildcards in a string or path using a dictionary of values.
With multiple wildcards, all possible combinations of values are created.
Parameters
----------
s : str | Path
The string or path to resolve wildcards in.
wildcards : dict[str, list[str]]
The dictionary of wildcards and values.
"""
# keep only wildcards in the string
wildcards_in_string = get_wildcards(s)
if not wildcards_in_string:
return s
is_path = False
if isinstance(s, Path):
is_path = True
s = s.as_posix()
# check if all wildcards are in the wildcards dict
missing_wildcards = set(wildcards_in_string) - set(wildcards.keys())
if missing_wildcards:
missing_wildcards_str = ", ".join(missing_wildcards)
raise KeyError(f"Wildcard values missing for: {missing_wildcards_str}")
wildcards = {k: v for k, v in wildcards.items() if k in wildcards_in_string}
single_value = all(isinstance(v, str) for v in wildcards.values())
# make sure values are lists
wildcards = {k: v if isinstance(v, list) else [v] for k, v in wildcards.items()}
resolved_strings = []
for wc in wildcard_product(wildcards):
resolved_strings.append(s.format(**wc))
if is_path:
resolved_strings = [Path(p) for p in resolved_strings]
if single_value:
return resolved_strings[0]
else:
return resolved_strings