from dataclasses import asdict, dataclass, field from pathlib import Path # from typing import Self import json from . import version as transpose_version from .exceptions import TransposeError from .utils import move, remove, symlink @dataclass class TransposeEntry: name: str path: str @dataclass class TransposeConfig: entries: dict = field(default_factory=dict) version: str = field(default=transpose_version) def add(self, name: str, path: str) -> None: """ Add a new entry to the entries Args: name: The name of the entry (must not exist) path: The path where the entry originally exists Returns: None """ if self.entries.get(name): raise TransposeError(f"'{name}' already exists") self.entries[name] = TransposeEntry(name=name, path=path) def get(self, name: str) -> TransposeEntry: """ Get an entry by the name Args: name: The name of the entry (must exist) Returns: TransposeEntry """ try: return self.entries[name] except KeyError: raise TransposeError(f"'{name}' does not exist in Transpose config entries") def remove(self, name: str) -> None: """ Remove an entry by name Args: name: The name of the entry (must exist) Returns: None """ try: del self.entries[name] except KeyError: raise TransposeError(f"'{name}' does not exist in Transpose config entries") def update(self, name: str, path: str) -> None: """ Update an entry by name Args: name: The name of the entry (must exist) path: The path where the entry originally exists Returns: None """ try: self.entries[name].path = path except KeyError: raise TransposeError(f"'{name}' does not exist in Transpose config entries") @staticmethod def load(config_path: str): # -> Self: in_config = json.load(open(config_path, "r")) config = TransposeConfig() try: for name in in_config["entries"]: config.add(name, in_config["entries"][name]["path"]) except (KeyError, TypeError) as e: raise TransposeError(f"Unrecognized Transpose config file format: {e}") return config def save(self, config_path: str) -> None: """ Save the Config to a location in JSON format Args: path: The path to save the json file Returns: None """ config_path = Path(config_path) config_path.parent.mkdir(parents=True, exist_ok=True) with open(str(config_path), "w") as f: json.dump(self.to_dict(), f) def to_dict(self) -> dict: return asdict(self) class Transpose: config: TransposeConfig config_path: Path store_path: Path def __init__(self, config_path: str) -> None: self.config = TransposeConfig.load(config_path) self.config_path = Path(config_path) self.store_path = self.config_path.parent if not self.store_path.exists(): self.store_path.mkdir(parents=True) def apply(self, name: str, force: bool = False) -> None: """ Create/recreate the symlink to an existing entry Args: name: The name of the entry (must exist) force: If enabled and path already exists, move the path to '{path}-bak' Returns: None """ if not self.config.entries.get(name): raise TransposeError(f"Entry does not exist: '{name}'") if self.config.entries[name].path.exists(): if self.config.entries[name].path.is_symlink(): remove(self.config.entries[name].path) elif force: # Backup the existing path, just in case move( self.config.entries[name].path, self.config.entries[name].path.joinpath("-bak"), ) else: raise TransposeError( f"Entry path already exists, cannot restore (force required): '{self.config.entries[name].path}'" ) symlink( target_path=self.store_path.joinpath(name), symlink_path=self.config.entries[name].path, ) def restore(self, name: str, force: bool = False) -> None: """ Remove the symlink and move the stored entry back to it's original path Args: name: The name of the entry (must exist) force: If enabled and path already exists, move the path to '{path}-bak' Returns: None """ if not self.config.entries.get(name): raise TransposeError(f"Could not locate entry by name: '{name}'") if self.config.entries[name].path.exists(): if self.config.entries[name].path.is_symlink(): remove(self.config.entries[name].path) elif force: # Backup the existing path, just in case move( self.config.entries[name].path, self.config.entries[name].path.joinpath("-bak"), ) else: raise TransposeError( f"Entry path already exists, cannot restore (force required): '{self.config.entries[name].path}'" ) move(self.store_path.joinpath(name), self.config.entries[name].path) self.config.remove(name) self.config.save(self.config_path) def store(self, name: str, source_path: str) -> None: """ Move the source path to the store path, create a symlink, and update the config Args: name: The name of the entry (must exist) source_path: The directory or file to be stored Returns: None """ if self.config.entries.get(name): raise TransposeError( f"Entry already exists: '{name}' ({self.config.entries[name].path})" ) storage_path = self.store_path.joinpath(name) if storage_path.exists(): raise TransposeError(f"Store path already exists: '{storage_path}'") source_path = self.config.entries[name].path if not source_path.exists(): raise TransposeError(f"Source path does not exist: '{source_path}'") if not source_path.is_dir() and not source_path.is_file(): raise TransposeError( f"Source path must be a directory or file: '{source_path}'" ) move(source=source_path, destination=storage_path) symlink(target_path=storage_path, symlink_path=source_path) self.config.add(name, source_path) self.config.save(self.config_path)