""" Issue domain models. Contains core business entities and value objects for issue management. """ from dataclasses import dataclass from typing import List, Optional from datetime import datetime, timezone from enum import Enum from .exceptions import IssueStateError class IssueState(Enum): """Issue state enumeration.""" OPEN = "open" CLOSED = "closed" IN_PROGRESS = "in_progress" @dataclass(frozen=True) class Label: """Value object representing an issue label.""" name: str color: Optional[str] = None description: Optional[str] = None def is_state_label(self) -> bool: """Check if this is a state-related label.""" return self.name.startswith('status:') def is_priority_label(self) -> bool: """Check if this is a priority-related label.""" return self.name.startswith('priority:') def is_type_label(self) -> bool: """Check if this is a type-related label.""" return self.name in ['bug', 'enhancement', 'feature', 'documentation'] @dataclass(frozen=True) class LabelCategories: """Value object for categorized labels.""" state_labels: List[str] priority_labels: List[str] type_labels: List[str] other_labels: List[str] @dataclass class Issue: """Issue aggregate root.""" number: int title: str state: IssueState labels: List[Label] created_at: datetime updated_at: datetime milestone: Optional[str] = None assignee: Optional[str] = None closed_at: Optional[datetime] = None def categorize_labels(self) -> LabelCategories: """Categorize labels by type - pure domain logic.""" state_labels = [label.name for label in self.labels if label.is_state_label()] priority_labels = [label.name for label in self.labels if label.is_priority_label()] type_labels = [label.name for label in self.labels if label.is_type_label()] other_labels = [ label.name for label in self.labels if not (label.is_state_label() or label.is_priority_label() or label.is_type_label()) ] return LabelCategories( state_labels=state_labels, priority_labels=priority_labels, type_labels=type_labels, other_labels=other_labels ) def close(self) -> None: """Close the issue - domain business rule.""" if self.state == IssueState.CLOSED: raise IssueStateError( "Issue is already closed", current_state=self.state.value, attempted_state=IssueState.CLOSED.value ) self.state = IssueState.CLOSED self.closed_at = datetime.now(timezone.utc) def reopen(self) -> None: """Reopen the issue - domain business rule.""" if self.state != IssueState.CLOSED: raise IssueStateError( "Issue is not closed", current_state=self.state.value, attempted_state=IssueState.OPEN.value ) self.state = IssueState.OPEN self.closed_at = None def add_label(self, label: Label) -> None: """Add a label to the issue.""" if label not in self.labels: self.labels.append(label) def remove_label(self, label_name: str) -> None: """Remove a label from the issue.""" self.labels = [label for label in self.labels if label.name != label_name] def has_label(self, label_name: str) -> bool: """Check if issue has a specific label.""" return any(label.name == label_name for label in self.labels)