Source code for xnat_ingest.model.scan

import logging
import re
import typing as ty
from pathlib import Path

import attrs
from fileformats.core import FileSet
from typing_extensions import Self

from ..helpers.arg_types import AssociatedFiles
from ..helpers.metadata import Metadata
from .resource import ImagingResource

logger = logging.getLogger("xnat-ingest")


if ty.TYPE_CHECKING:
    from xnat_ingest.model.session import ImagingSession


def scan_type_converter(scan_type: str | None) -> str | None:
    "Ensure there aren't any special characters that aren't valid file/dir paths"
    if scan_type is None:
        return None
    return re.sub(r"[\"\*\/\:\<\>\?\\\|\+\,\.\;\=\[\]]+", "", scan_type)


def scan_resources_converter(
    resources: dict[str, ImagingResource | FileSet],
) -> ty.Dict[str, ImagingResource]:
    return {
        scan_type_converter(k): (
            v if isinstance(v, ImagingResource) else ImagingResource(k, v)
        )
        for k, v in resources.items()
    }


[docs] @attrs.define class ImagingScan: """Representation of a scan to be uploaded to XNAT Parameters ---------- id: str the ID of the scan on XNAT type: str | None the scan type/description resources : dict[str, ImagingResource] the list of the resources associated with the scan associated: AssociatedFiles, optional Secondary files that are associated with the given imaging scan (but may not have the necessary metadata to identify the scan itself) """ id: str type: str = attrs.field(converter=scan_type_converter, default=None) resources: ty.Dict[str, ImagingResource] = attrs.field( factory=dict, converter=scan_resources_converter ) metadata: Metadata = attrs.field(eq=False, repr=False, init=False) associated: AssociatedFiles | None = None # Back-ref to parent session session: "ImagingSession" = attrs.field(default=None, eq=False, repr=False) def __contains__(self, resource_name: str) -> bool: return resource_name in self.resources def __getitem__(self, resource_name: str) -> ImagingResource: return self.resources[resource_name] def __attrs_post_init__(self) -> None: for resource in self.resources.values(): resource.scan = self def new_empty(self) -> Self: return type(self)(self.id, self.type) @metadata.default def _metadata_default(self) -> Metadata: return Metadata({}, self) def load_metadata(self): return Metadata.collate(r.metadata for r in self.resources.values())
[docs] def save( self, dest_dir: Path, copy_mode: FileSet.CopyMode = FileSet.CopyMode.hardlink_or_copy, collation_map: dict[ty.Type[FileSet], FileSet.CopyCollation] | None = None, ) -> Self: # Ensure scan type is a valid directory name. A scan with no description set # (e.g. not yet resolved by 'assign') is saved as '<scan_id>.' (trailing dot, # no description) to still distinguish it from session-level resource dirs saved = self.new_empty() scan_dir = dest_dir / f"{self.id}.{self.type if self.type is not None else ''}" scan_dir.mkdir(parents=True, exist_ok=True) for resource in self.resources.values(): saved_resource = resource.save( scan_dir, copy_mode=copy_mode, collation_map=collation_map ) saved_resource.scan = saved saved.resources[saved_resource.name] = saved_resource self.metadata.save(scan_dir) return saved
[docs] @classmethod def load( cls, scan_dir: Path, require_manifest: bool = True, check_checksums: bool = True ) -> Self: scan_id, scan_type = scan_dir.name.split(".", 1) scan = cls(scan_id, scan_type or None) for resource_dir in scan_dir.iterdir(): if resource_dir.is_dir(): resource = ImagingResource.load( resource_dir, require_manifest=require_manifest, check_checksums=check_checksums, ) resource.scan = scan scan.resources[resource.name] = resource if (scan_dir / Metadata.FNAME).exists(): scan.metadata = Metadata.load(scan_dir, scan) return scan
@property def path(self) -> str: return self.session.path + ":" + self.id + "-" + self.type