Files
markitect-main/issue-facade/core/interfaces.py
tegwick cb94c92fc0 feat: implement universal issue tracking facade
Add comprehensive issue tracking facade system that provides a unified CLI interface to any issue tracking backend. The facade automatically detects the repository's issue tracker and provides consistent commands across all platforms.

Key features:
- Repository-aware automatic backend detection (GitHub, GitLab, Gitea, local SQLite)
- Unified CLI interface with same commands across all backends
- Plugin architecture for extensible backend support
- Local SQLite backend for offline development
- Gitea backend with full API integration
- Bidirectional synchronization between backends
- Performance-optimized domain models with caching
- Clean architecture with separation of concerns

The facade acts as a "universal remote control" for issue tracking systems, eliminating the need to learn different CLIs for each platform while providing seamless offline capability and cross-platform consistency.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 21:04:43 +02:00

407 lines
11 KiB
Python

"""
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())