""" Backend Plugin Interfaces Defines the contracts that all issue tracking backend plugins must implement. This enables a clean plugin architecture where new backends can be added without modifying core code. """ from abc import ABC, abstractmethod from typing import List, Optional, Dict, Any, Iterator from datetime import datetime from .models import Issue, Label, User, Milestone, Comment class IssueFilter: """Filter criteria for issue queries.""" def __init__( self, state: Optional[str] = None, assignee: Optional[str] = None, labels: Optional[List[str]] = None, milestone: Optional[str] = None, created_after: Optional[datetime] = None, created_before: Optional[datetime] = None, updated_after: Optional[datetime] = None, updated_before: Optional[datetime] = None, search: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = 0 ): self.state = state self.assignee = assignee self.labels = labels or [] self.milestone = milestone self.created_after = created_after self.created_before = created_before self.updated_after = updated_after self.updated_before = updated_before self.search = search self.limit = limit self.offset = offset class BackendCapabilities: """Describes what features a backend supports.""" def __init__( self, supports_milestones: bool = True, supports_assignees: bool = True, supports_comments: bool = True, supports_labels: bool = True, supports_search: bool = True, supports_bulk_operations: bool = False, supports_webhooks: bool = False, supports_real_time: bool = False, max_labels_per_issue: Optional[int] = None, max_assignees_per_issue: Optional[int] = None ): self.supports_milestones = supports_milestones self.supports_assignees = supports_assignees self.supports_comments = supports_comments self.supports_labels = supports_labels self.supports_search = supports_search self.supports_bulk_operations = supports_bulk_operations self.supports_webhooks = supports_webhooks self.supports_real_time = supports_real_time self.max_labels_per_issue = max_labels_per_issue self.max_assignees_per_issue = max_assignees_per_issue class IssueBackend(ABC): """ Abstract base class for all issue tracking backends. Each backend plugin must implement this interface to provide issue tracking functionality for a specific system. """ @property @abstractmethod def backend_type(self) -> str: """Return the backend type identifier (e.g., 'local', 'gitea', 'github').""" pass @property @abstractmethod def capabilities(self) -> BackendCapabilities: """Return the capabilities supported by this backend.""" pass @abstractmethod def connect(self, config: Dict[str, Any]) -> None: """ Connect to the backend using provided configuration. Args: config: Backend-specific configuration (URLs, tokens, etc.) """ pass @abstractmethod def disconnect(self) -> None: """Disconnect from the backend and clean up resources.""" pass @abstractmethod def test_connection(self) -> bool: """Test if the backend connection is working.""" pass # Issue CRUD Operations @abstractmethod def create_issue(self, issue: Issue) -> Issue: """ Create a new issue in the backend. Args: issue: Issue to create (id may be None for new issues) Returns: Created issue with backend_id populated """ pass @abstractmethod def get_issue(self, issue_id: str) -> Optional[Issue]: """ Retrieve an issue by its backend ID. Args: issue_id: Backend-specific issue ID Returns: Issue if found, None otherwise """ pass @abstractmethod def get_issue_by_number(self, number: int) -> Optional[Issue]: """ Retrieve an issue by its human-readable number. Args: number: Issue number Returns: Issue if found, None otherwise """ pass @abstractmethod def update_issue(self, issue: Issue) -> Issue: """ Update an existing issue in the backend. Args: issue: Issue with modifications Returns: Updated issue """ pass @abstractmethod def delete_issue(self, issue_id: str) -> bool: """ Delete an issue from the backend. Args: issue_id: Backend-specific issue ID Returns: True if deleted successfully """ pass @abstractmethod def list_issues(self, filter_criteria: Optional[IssueFilter] = None) -> List[Issue]: """ List issues matching filter criteria. Args: filter_criteria: Optional filter to apply Returns: List of matching issues """ pass @abstractmethod def search_issues(self, query: str, limit: Optional[int] = None) -> List[Issue]: """ Search issues using backend-specific query syntax. Args: query: Search query limit: Maximum number of results Returns: List of matching issues """ pass # Label Operations @abstractmethod def create_label(self, label: Label) -> Label: """Create a new label.""" pass @abstractmethod def get_labels(self) -> List[Label]: """Get all available labels.""" pass @abstractmethod def update_label(self, label: Label) -> Label: """Update an existing label.""" pass @abstractmethod def delete_label(self, label_name: str) -> bool: """Delete a label.""" pass # User Operations @abstractmethod def get_users(self) -> List[User]: """Get all available users.""" pass @abstractmethod def get_user(self, user_id: str) -> Optional[User]: """Get a specific user by ID.""" pass @abstractmethod def search_users(self, query: str) -> List[User]: """Search for users.""" pass # Milestone Operations @abstractmethod def create_milestone(self, milestone: Milestone) -> Milestone: """Create a new milestone.""" pass @abstractmethod def get_milestones(self) -> List[Milestone]: """Get all milestones.""" pass @abstractmethod def update_milestone(self, milestone: Milestone) -> Milestone: """Update a milestone.""" pass @abstractmethod def delete_milestone(self, milestone_id: str) -> bool: """Delete a milestone.""" pass # Comment Operations @abstractmethod def add_comment(self, issue_id: str, comment: Comment) -> Comment: """Add a comment to an issue.""" pass @abstractmethod def get_comments(self, issue_id: str) -> List[Comment]: """Get all comments for an issue.""" pass @abstractmethod def update_comment(self, comment: Comment) -> Comment: """Update a comment.""" pass @abstractmethod def delete_comment(self, comment_id: str) -> bool: """Delete a comment.""" pass # Bulk Operations (optional, depends on capabilities) def bulk_update_issues(self, updates: List[Dict[str, Any]]) -> List[Issue]: """ Bulk update multiple issues. Args: updates: List of update operations Returns: List of updated issues """ if not self.capabilities.supports_bulk_operations: raise NotImplementedError(f"{self.backend_type} backend does not support bulk operations") # Default implementation: update one by one results = [] for update in updates: issue_id = update['id'] issue = self.get_issue(issue_id) if issue: # Apply updates to issue for key, value in update.items(): if key != 'id' and hasattr(issue, key): setattr(issue, key, value) updated_issue = self.update_issue(issue) results.append(updated_issue) return results # Sync Support def get_last_sync_timestamp(self) -> Optional[datetime]: """ Get the timestamp of the last successful sync. Used for incremental synchronization. """ return None def get_issues_modified_since(self, timestamp: datetime) -> List[Issue]: """ Get issues modified since a specific timestamp. Used for incremental synchronization. """ filter_criteria = IssueFilter(updated_after=timestamp) return self.list_issues(filter_criteria) class LocalBackend(IssueBackend): """ Marker interface for local backends. Local backends store data locally and can work offline. They serve as the source of truth for synchronization. """ pass class RemoteBackend(IssueBackend): """ Marker interface for remote backends. Remote backends connect to external issue tracking systems. They participate in bidirectional synchronization. """ pass class SyncableBackend(ABC): """ Interface for backends that support synchronization. Backends implementing this interface can participate in bidirectional sync operations. """ @abstractmethod def prepare_for_sync(self) -> None: """Prepare backend for sync operation (e.g., create backup).""" pass @abstractmethod def finalize_sync(self, success: bool) -> None: """Finalize sync operation (e.g., commit or rollback).""" pass @abstractmethod def get_sync_conflicts(self) -> List[Dict[str, Any]]: """Get issues that have sync conflicts.""" pass @abstractmethod def resolve_sync_conflict(self, issue_id: str, resolution: str) -> Issue: """ Resolve a sync conflict. Args: issue_id: Issue with conflict resolution: 'local' or 'remote' or 'merge' """ pass class BackendFactory: """Factory for creating backend instances.""" _backends: Dict[str, type] = {} @classmethod def register_backend(cls, backend_type: str, backend_class: type) -> None: """Register a backend implementation.""" cls._backends[backend_type] = backend_class @classmethod def create_backend(cls, backend_type: str) -> IssueBackend: """Create a backend instance.""" if backend_type not in cls._backends: raise ValueError(f"Unknown backend type: {backend_type}") return cls._backends[backend_type]() @classmethod def get_available_backends(cls) -> List[str]: """Get list of available backend types.""" return list(cls._backends.keys())