"""Pure action functions for the data module (variables + environment)."""
from __future__ import annotations
import json
import os
import re
import time
from pathlib import Path
from typing import Any
from steplib.core.exceptions import MissingDependencyError
from steplib.modules.data.context import DataContext
def _normalize_value(value: Any) -> str:
"""Normalize a value to its string representation for comparison.
Python's ``str(True)`` returns ``"True"``, but users naturally write
``"true"`` / ``"false"`` / ``"null"`` in step definitions. This helper
ensures booleans and ``None`` use their JSON-style lowercase
representation.
"""
if value is True:
return "true"
if value is False:
return "false"
if value is None:
return "null"
return str(value)
# --- Variable actions ---
[docs]
def data_set_variable(data_ctx: DataContext, name: str, value: str) -> None:
"""Set a generic variable in the data context.
Args:
data_ctx: The data context to operate on.
name: The variable name.
value: The variable value (stored as string).
"""
data_ctx.variables[name] = value
[docs]
def data_assert_variable_equals(
data_ctx: DataContext,
name: str,
expected: str,
) -> None:
"""Assert that a variable equals an expected value.
Args:
data_ctx: The data context to check.
name: The variable name.
expected: The expected value.
Raises:
AssertionError: If the variable does not exist or the value differs.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
actual = data_ctx.variables[name]
if _normalize_value(actual) != _normalize_value(expected):
raise AssertionError(
f"Variable '{name}': expected '{expected}', got '{actual}'."
)
[docs]
def data_assert_variable_exists(data_ctx: DataContext, name: str) -> None:
"""Assert that a variable exists in the data context.
Args:
data_ctx: The data context to check.
name: The variable name.
Raises:
AssertionError: If the variable does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
[docs]
def data_assert_variable_not_exists(data_ctx: DataContext, name: str) -> None:
"""Assert that a variable does not exist in the data context.
Args:
data_ctx: The data context to check.
name: The variable name.
Raises:
AssertionError: If the variable exists.
"""
if name in data_ctx.variables:
raise AssertionError(f"Variable '{name}' should not exist.")
[docs]
def data_delete_variable(data_ctx: DataContext, name: str) -> None:
"""Delete a variable from the data context.
Args:
data_ctx: The data context to operate on.
name: The variable name.
Raises:
KeyError: If the variable does not exist.
"""
if name not in data_ctx.variables:
raise KeyError(f"Variable '{name}' does not exist.")
del data_ctx.variables[name]
[docs]
def data_assert_variable_not_equals(
data_ctx: DataContext,
name: str,
expected: str,
) -> None:
"""Assert that a variable does not equal a value.
Args:
data_ctx: The data context to check.
name: The variable name.
expected: The value that the variable should NOT have.
Raises:
AssertionError: If the variable does not exist or equals the value.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
actual = data_ctx.variables[name]
if _normalize_value(actual) == _normalize_value(expected):
raise AssertionError(
f"Variable '{name}' should not equal '{expected}'."
)
[docs]
def data_assert_variable_contains(
data_ctx: DataContext,
name: str,
substring: str,
) -> None:
"""Assert that a variable's string value contains a substring.
Args:
data_ctx: The data context to check.
name: The variable name.
substring: The substring to look for.
Raises:
AssertionError: If the variable does not exist or doesn't contain the substring.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
actual = _normalize_value(data_ctx.variables[name])
if substring not in actual:
raise AssertionError(
f"Variable '{name}': expected to contain '{substring}', got '{actual}'."
)
[docs]
def data_assert_variable_is_empty(data_ctx: DataContext, name: str) -> None:
"""Assert that a variable is empty (empty string, empty list, empty dict, or None).
Args:
data_ctx: The data context to check.
name: The variable name.
Raises:
AssertionError: If the variable does not exist or is not empty.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = data_ctx.variables[name]
if value is not None and value not in ("", [], {}):
raise AssertionError(f"Variable '{name}' is not empty: {value!r}.")
[docs]
def data_assert_variable_is_not_empty(data_ctx: DataContext, name: str) -> None:
"""Assert that a variable is not empty.
Args:
data_ctx: The data context to check.
name: The variable name.
Raises:
AssertionError: If the variable does not exist or is empty.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = data_ctx.variables[name]
if value is None or value in ("", [], {}):
raise AssertionError(f"Variable '{name}' is empty.")
[docs]
def data_assert_variable_has_length(
data_ctx: DataContext,
name: str,
expected: int,
) -> None:
"""Assert that a variable has a specific length.
Works with strings, lists, dicts, and any object with ``__len__``.
Args:
data_ctx: The data context to check.
name: The variable name.
expected: The expected length.
Raises:
AssertionError: If the variable does not exist or has a different length.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = data_ctx.variables[name]
try:
actual = len(value)
except TypeError as exc:
raise AssertionError(
f"Variable '{name}' of type {type(value).__name__} has no length."
) from exc
if actual != expected:
raise AssertionError(
f"Variable '{name}': expected length {expected}, got {actual}."
)
[docs]
def data_copy_variable(
data_ctx: DataContext,
source: str,
target: str,
) -> None:
"""Copy a variable to a new name.
Args:
data_ctx: The data context to operate on.
source: The source variable name.
target: The target variable name.
Raises:
KeyError: If the source variable does not exist.
"""
if source not in data_ctx.variables:
raise KeyError(f"Source variable '{source}' does not exist.")
data_ctx.variables[target] = data_ctx.variables[source]
[docs]
def data_clear_variables(data_ctx: DataContext) -> None:
"""Clear all variables from the data context."""
data_ctx.variables = {}
[docs]
def data_set_variable_json(
data_ctx: DataContext,
name: str,
json_str: str,
) -> None:
"""Set a variable to a parsed JSON value.
Args:
data_ctx: The data context to operate on.
name: The variable name.
json_str: A JSON string to parse and store.
Raises:
json.JSONDecodeError: If the string is not valid JSON.
"""
data_ctx.variables[name] = json.loads(json_str)
[docs]
def data_set_env_from_variable(
data_ctx: DataContext,
variable: str,
key: str,
) -> None:
"""Set an environment variable from a data variable's value.
Args:
data_ctx: The data context (used for backup tracking).
variable: The source data variable name.
key: The environment variable name to set.
Raises:
KeyError: If the source variable does not exist.
"""
if variable not in data_ctx.variables:
raise KeyError(f"Variable '{variable}' does not exist.")
data_set_env_var(data_ctx, key, _normalize_value(data_ctx.variables[variable]))
[docs]
def data_load_yaml_file(data_ctx: DataContext, path: str, name: str) -> None:
"""Load a YAML file into a variable as a dict.
Args:
data_ctx: The data context to operate on.
path: Path to the YAML file.
name: The variable name to store the parsed content.
Raises:
MissingDependencyError: If PyYAML is not installed.
FileNotFoundError: If the file does not exist.
"""
try:
import yaml
except ImportError as exc:
raise MissingDependencyError("data", "pyyaml") from exc
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"YAML file not found: {path}")
with file_path.open(encoding="utf-8") as f:
data_ctx.variables[name] = yaml.safe_load(f)
[docs]
def data_load_json_file(data_ctx: DataContext, path: str, name: str) -> None:
"""Load a JSON file into a variable as a dict.
Args:
data_ctx: The data context to operate on.
path: Path to the JSON file.
name: The variable name to store the parsed content.
Raises:
FileNotFoundError: If the file does not exist.
"""
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"JSON file not found: {path}")
with file_path.open(encoding="utf-8") as f:
data_ctx.variables[name] = json.load(f)
# --- Environment variable actions ---
[docs]
def data_set_env_var(data_ctx: DataContext, key: str, value: str) -> None:
"""Set an environment variable, backing up the original for restoration.
Args:
data_ctx: The data context (used for backup tracking).
key: The environment variable name.
value: The value to set.
"""
if key not in data_ctx._env_backup:
data_ctx._env_backup[key] = os.environ.get(key)
os.environ[key] = value
[docs]
def data_delete_env_var(data_ctx: DataContext, key: str) -> None:
"""Delete an environment variable, backing up the original for restoration.
Args:
data_ctx: The data context (used for backup tracking).
key: The environment variable name.
"""
if key not in data_ctx._env_backup:
data_ctx._env_backup[key] = os.environ.get(key)
os.environ.pop(key, None)
[docs]
def data_assert_env_equals(key: str, expected: str) -> None:
"""Assert that an environment variable equals an expected value.
Args:
key: The environment variable name.
expected: The expected value.
Raises:
AssertionError: If the env var does not exist or differs.
"""
actual = os.environ.get(key)
if actual is None:
raise AssertionError(f"Environment variable '{key}' is not set.")
if actual != expected:
raise AssertionError(
f"Environment variable '{key}': expected '{expected}', got '{actual}'."
)
[docs]
def data_assert_env_exists(key: str) -> None:
"""Assert that an environment variable exists.
Args:
key: The environment variable name.
Raises:
AssertionError: If the env var does not exist.
"""
if key not in os.environ:
raise AssertionError(f"Environment variable '{key}' is not set.")
[docs]
def data_assert_env_not_equals(key: str, expected: str) -> None:
"""Assert that an environment variable does not equal a value.
Args:
key: The environment variable name.
expected: The value that the env var should NOT have.
Raises:
AssertionError: If the env var does not exist or equals the value.
"""
actual = os.environ.get(key)
if actual is None:
raise AssertionError(f"Environment variable '{key}' is not set.")
if actual == expected:
raise AssertionError(
f"Environment variable '{key}' should not equal '{expected}'."
)
[docs]
def data_assert_env_not_exists(key: str) -> None:
"""Assert that an environment variable does not exist.
Args:
key: The environment variable name.
Raises:
AssertionError: If the env var exists.
"""
if key in os.environ:
raise AssertionError(f"Environment variable '{key}' should not be set.")
[docs]
def data_store_env_var(
data_ctx: DataContext,
key: str,
variable: str,
) -> None:
"""Store an environment variable's value into a data variable.
Args:
data_ctx: The data context to operate on.
key: The environment variable name.
variable: The target variable name.
Raises:
AssertionError: If the env var does not exist.
"""
if key not in os.environ:
raise AssertionError(f"Environment variable '{key}' is not set.")
data_ctx.variables[variable] = os.environ[key]
[docs]
def data_load_env_file(data_ctx: DataContext, path: str) -> None:
"""Load environment variables from a .env-style file.
Parses simple ``KEY=VALUE`` lines. Lines starting with ``#`` are
ignored. Quoted values (single or double) are unquoted.
Args:
data_ctx: The data context (used for backup tracking).
path: Path to the .env file.
Raises:
FileNotFoundError: If the file does not exist.
"""
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"Env file not found: {path}")
with file_path.open(encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1]
data_set_env_var(data_ctx, key, value)
# --- Extended variable assertions ---
[docs]
def data_assert_variable_matches(
data_ctx: DataContext, name: str, pattern: str
) -> None:
"""Assert that a variable's string value matches a regex pattern.
Args:
data_ctx: The data context.
name: Variable name.
pattern: Regex pattern to match.
Raises:
AssertionError: If the variable does not match or does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = _normalize_value(data_ctx.variables[name])
try:
if not re.search(pattern, value):
raise AssertionError(
f"Variable '{name}' value '{value}' does not match pattern '{pattern}'."
)
except re.error as exc:
raise AssertionError(f"Invalid regex pattern '{pattern}': {exc}") from exc
[docs]
def data_assert_variable_starts_with(
data_ctx: DataContext, name: str, text: str
) -> None:
"""Assert that a variable's string value starts with the given text.
Args:
data_ctx: The data context.
name: Variable name.
text: Expected prefix.
Raises:
AssertionError: If the variable does not start with the text or does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = _normalize_value(data_ctx.variables[name])
if not value.startswith(text):
raise AssertionError(
f"Variable '{name}' value '{value}' does not start with '{text}'."
)
[docs]
def data_assert_variable_ends_with(
data_ctx: DataContext, name: str, text: str
) -> None:
"""Assert that a variable's string value ends with the given text.
Args:
data_ctx: The data context.
name: Variable name.
text: Expected suffix.
Raises:
AssertionError: If the variable does not end with the text or does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
value = _normalize_value(data_ctx.variables[name])
if not value.endswith(text):
raise AssertionError(
f"Variable '{name}' value '{value}' does not end with '{text}'."
)
[docs]
def data_increment_variable(
data_ctx: DataContext, name: str, amount: int = 1
) -> None:
"""Increment a numeric variable by a given amount.
Args:
data_ctx: The data context.
name: Variable name.
amount: Amount to increment (default 1).
Raises:
KeyError: If the variable does not exist.
ValueError: If the variable is not numeric.
"""
if name not in data_ctx.variables:
raise KeyError(f"Variable '{name}' does not exist.")
current = data_ctx.variables[name]
if isinstance(current, bool):
raise ValueError(
f"Variable '{name}' value '{current}' is a boolean, not numeric."
)
try:
if isinstance(current, float):
numeric: int | float = current + amount
else:
try:
numeric = int(current) + amount
except (TypeError, ValueError):
numeric = float(current) + amount
except (TypeError, ValueError) as exc:
raise ValueError(
f"Variable '{name}' value '{current}' is not numeric."
) from exc
data_ctx.variables[name] = numeric
[docs]
def data_assert_variable_greater_than(
data_ctx: DataContext, name: str, value: str
) -> None:
"""Assert that a variable's numeric value is greater than a threshold.
Args:
data_ctx: The data context.
name: Variable name.
value: Threshold value (compared as float).
Raises:
AssertionError: If the variable is not greater than the value or does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
raw = data_ctx.variables[name]
if isinstance(raw, bool):
raise AssertionError(
f"Variable '{name}' is a boolean, not a numeric value."
)
try:
current = float(raw)
threshold = float(value)
except (TypeError, ValueError) as exc:
raise AssertionError(
f"Variable '{name}' or threshold '{value}' is not numeric."
) from exc
if not current > threshold:
raise AssertionError(
f"Variable '{name}' value {current} is not greater than {threshold}."
)
[docs]
def data_assert_variable_less_than(
data_ctx: DataContext, name: str, value: str
) -> None:
"""Assert that a variable's numeric value is less than a threshold.
Args:
data_ctx: The data context.
name: Variable name.
value: Threshold value (compared as float).
Raises:
AssertionError: If the variable is not less than the value or does not exist.
"""
if name not in data_ctx.variables:
raise AssertionError(f"Variable '{name}' does not exist.")
raw = data_ctx.variables[name]
if isinstance(raw, bool):
raise AssertionError(
f"Variable '{name}' is a boolean, not a numeric value."
)
try:
current = float(raw)
threshold = float(value)
except (TypeError, ValueError) as exc:
raise AssertionError(
f"Variable '{name}' or threshold '{value}' is not numeric."
) from exc
if not current < threshold:
raise AssertionError(
f"Variable '{name}' value {current} is not less than {threshold}."
)
# --- Utility actions ---
[docs]
def data_wait(seconds: float) -> None:
"""Sleep for a given number of seconds.
Args:
seconds: Number of seconds to sleep.
"""
time.sleep(seconds)