"""Library for loading structure data from various sources.""" from collections.abc import Mapping, Sequence import functools import os import pathlib import tarfile class NotFoundError(KeyError): """Raised when the structure store doesn't contain the requested target.""" class StructureStore: """Handles the retrieval of mmCIF files from a filesystem.""" def __init__( self, structures: str | os.PathLike[str] | Mapping[str, str], ): """Initialises the instance. Args: structures: Path of the directory where the mmCIF files are or a Mapping from target name to mmCIF string. """ if isinstance(structures, Mapping): self._structure_mapping = structures self._structure_path = None self._structure_tar = None else: self._structure_mapping = None path_str = os.fspath(structures) if path_str.endswith('.tar'): self._structure_tar = tarfile.open(path_str, 'r') self._structure_path = None else: self._structure_path = pathlib.Path(structures) self._structure_tar = None @functools.cached_property def _tar_members(self) -> Mapping[str, tarfile.TarInfo]: assert self._structure_tar is not None return { path.stem: tarinfo for tarinfo in self._structure_tar.getmembers() if tarinfo.isfile() and (path := pathlib.Path(tarinfo.path.lower())).suffix == '.cif' } def get_mmcif_str(self, target_name: str) -> str: """Returns an mmCIF for a given `target_name`. Args: target_name: Name specifying the target mmCIF. Raises: NotFoundError: If the target is not found. """ if self._structure_mapping is not None: try: return self._structure_mapping[target_name] except KeyError as e: raise NotFoundError(f'{target_name=} not found') from e if self._structure_tar is not None: try: member = self._tar_members[target_name] if struct_file := self._structure_tar.extractfile(member): return struct_file.read().decode() else: raise NotFoundError(f'{target_name=} not found') except KeyError: raise NotFoundError(f'{target_name=} not found') from None filepath = self._structure_path / f'{target_name}.cif' try: return filepath.read_text() except FileNotFoundError as e: raise NotFoundError(f'{target_name=} not found at {filepath=}') from e def target_names(self) -> Sequence[str]: """Returns all targets in the store.""" if self._structure_mapping is not None: return [*self._structure_mapping.keys()] elif self._structure_tar is not None: return sorted(self._tar_members.keys()) elif self._structure_path is not None: return sorted([path.stem for path in self._structure_path.glob('*.cif')]) return ()