Source code for idtap.classes.assemblage

from __future__ import annotations
from typing import List, Optional, Dict, TypedDict
import uuid

from .phrase import Phrase
from ..enums import Instrument


class StrandDescriptorType(TypedDict):
    label: str
    phraseIDs: List[str]
    id: str


class AssemblageDescriptorType(TypedDict):
    instrument: Instrument
    strands: List[StrandDescriptorType]
    name: str
    id: str
    loosePhraseIDs: List[str]


class Assemblage:  # forward declaration for Strand type hints
    pass


class Strand:
    def __init__(self, label: str, phrase_ids: List[str], assemblage: Assemblage, id: Optional[str] = None) -> None:
        self.label = label
        self.phrase_ids: List[str] = phrase_ids
        self.assemblage = assemblage
        self.id = id or str(uuid.uuid4())
        self.name_editing: bool = False

    # ------------------------------------------------------------------
    def add_phrase(self, phrase: Phrase) -> None:
        if phrase.unique_id in self.phrase_ids:
            raise Exception(f"Phrase with UUID {phrase.unique_id} already exists in strand {self.label}")
        self.phrase_ids.append(phrase.unique_id)

    # ------------------------------------------------------------------
    def remove_phrase(self, phrase: Phrase) -> None:
        try:
            self.phrase_ids.remove(phrase.unique_id)
        except ValueError:
            raise Exception(f"Phrase with UUID {phrase.unique_id} not found in strand {self.label}")

    # ------------------------------------------------------------------
    @property
    def phrases(self) -> List[Phrase]:
        phrases = []
        for uid in self.phrase_ids:
            match = next((p for p in self.assemblage.phrases if p.unique_id == uid), None)
            if match is not None:
                phrases.append(match)
        phrases.sort(key=lambda p: p.start_time or 0)
        return phrases


[docs] class Assemblage:
[docs] def __init__(self, instrument: Instrument, name: str, id: Optional[str] = None) -> None: # Parameter validation self._validate_parameters({'instrument': instrument, 'name': name, 'id': id}) self.phrases: List[Phrase] = [] self.strands: List[Strand] = [] self.instrument = instrument self.name = name self.id = id or str(uuid.uuid4())
def _validate_parameters(self, opts: dict) -> None: """Validate constructor parameters and provide helpful error messages.""" if 'instrument' in opts: if not isinstance(opts['instrument'], Instrument): raise TypeError(f"Parameter 'instrument' must be an Instrument enum, got {type(opts['instrument']).__name__}") if 'name' in opts: if not isinstance(opts['name'], str): raise TypeError(f"Parameter 'name' must be a string, got {type(opts['name']).__name__}") if opts['name'] == "": raise ValueError("Parameter 'name' cannot be empty") if 'id' in opts and opts['id'] is not None: if not isinstance(opts['id'], str): raise TypeError(f"Parameter 'id' must be a string, got {type(opts['id']).__name__}") # ------------------------------------------------------------------
[docs] def add_strand(self, label: str, id: Optional[str] = None) -> None: if any(s.label == label for s in self.strands): raise Exception(f"Strand with label {label} already exists") self.strands.append(Strand(label, [], self, id))
# ------------------------------------------------------------------
[docs] def add_phrase(self, phrase: Phrase, strand_id: Optional[str] = None) -> None: if any(p.unique_id == phrase.unique_id for p in self.phrases): raise Exception(f"Phrase with UUID {phrase.unique_id} already exists in assemblage") self.phrases.append(phrase) if strand_id is None: return strand = next((s for s in self.strands if s.id == strand_id), None) if strand is None: raise Exception(f"Strand with id {strand_id} not found") strand.add_phrase(phrase)
# ------------------------------------------------------------------
[docs] def remove_strand(self, id: str) -> None: idx = next((i for i, s in enumerate(self.strands) if s.id == id), -1) if idx == -1: raise Exception(f"Strand with id {id} not found") self.strands.pop(idx)
# ------------------------------------------------------------------
[docs] def move_phrase_to_strand(self, phrase: Phrase, target_strand_id: Optional[str] = None) -> None: source = next((s for s in self.strands if phrase.unique_id in s.phrase_ids), None) target = next((s for s in self.strands if s.id == target_strand_id), None) if target is None: if source: source.remove_phrase(phrase) return if source is None: if phrase not in self.phrases: raise Exception(f"Phrase with UUID {phrase.unique_id} not found in assemblage") target.add_phrase(phrase) else: source.remove_phrase(phrase) target.add_phrase(phrase)
# ------------------------------------------------------------------
[docs] def remove_phrase(self, phrase: Phrase) -> None: if phrase not in self.phrases: raise Exception(f"Phrase with UUID {phrase.unique_id} not found in assemblage") for strand in self.strands: if phrase.unique_id in strand.phrase_ids: strand.remove_phrase(phrase) self.phrases.remove(phrase)
# ------------------------------------------------------------------
[docs] @staticmethod def from_descriptor(descriptor: AssemblageDescriptorType, phrases: List[Phrase]) -> 'Assemblage': assemblage = Assemblage(descriptor['instrument'], descriptor['name'], descriptor['id']) for strand_desc in descriptor['strands']: assemblage.add_strand(strand_desc['label'], strand_desc['id']) for pid in strand_desc['phraseIDs']: match = next((p for p in phrases if p.unique_id == pid), None) if match is None: raise Exception(f"Phrase with UUID {pid} not found") assemblage.add_phrase(match, strand_desc['id']) for pid in descriptor['loosePhraseIDs']: match = next((p for p in phrases if p.unique_id == pid), None) if match is None: raise Exception(f"Loose phrase with UUID {pid} not found") assemblage.add_phrase(match) return assemblage
# ------------------------------------------------------------------ @property def loose_phrases(self) -> List[Phrase]: attached_ids = {pid for s in self.strands for pid in s.phrase_ids} loose = [p for p in self.phrases if p.unique_id not in attached_ids] loose.sort(key=lambda p: p.start_time or 0) return loose # ------------------------------------------------------------------ @property def descriptor(self) -> AssemblageDescriptorType: return { 'instrument': self.instrument, 'strands': [ {'label': s.label, 'phraseIDs': list(s.phrase_ids), 'id': s.id} for s in self.strands ], 'name': self.name, 'id': self.id, 'loosePhraseIDs': [p.unique_id for p in self.loose_phrases], }