import base64 from pathlib import Path from typing import Optional, Union from pydantic import BaseModel, Field class FieldPath(BaseModel): current: str tail: Optional[list["FieldPath"]] = Field(default=None) def as_str_list(self) -> list[str]: """ >>> FieldPath(current='a', tail=[FieldPath(current='b', tail=[FieldPath(current='c'), FieldPath(current='d')])]).as_str_list() ['a.b.c', 'a.b.d'] """ # Recursive function to collect all paths def collect_paths(path: FieldPath, prefix: str = "") -> list[str]: current_path = prefix + path.current if not path.tail: return [current_path] else: paths = [] for sub_path in path.tail: paths.extend(collect_paths(sub_path, current_path + ".")) return paths # Collect all paths starting from this object return collect_paths(self) def convert_paths(paths: list[str]) -> list[FieldPath]: """Convert string paths into FieldPath objects Paths which share the same root are grouped together. Args: paths: List[str]: List of str paths containing "." as separator Returns: List[FieldPath]: List of FieldPath objects """ sorted_paths = sorted(paths) prev_root = None converted_paths = [] for path in sorted_paths: parts = path.split(".") root = parts[0] if root != prev_root: converted_paths.append(FieldPath(current=root)) prev_root = root current = converted_paths[-1] for part in parts[1:]: if current.tail is None: current.tail = [] found = False for tail in current.tail: if tail.current == part: current = tail found = True break if not found: new_tail = FieldPath(current=part) assert current.tail is not None current.tail.append(new_tail) current = new_tail return converted_paths def read_base64(file_path: Union[str, Path]) -> str: """Convert a file path to a base64 encoded string.""" path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"The file {path} does not exist.") with open(path, "rb") as file: file_content = file.read() return base64.b64encode(file_content).decode("utf-8")