Skip to content

Ports & Adapters ​

API reference for spectryn's port interfaces and adapter implementations.

Ports (Interfaces) ​

Ports define abstract interfaces that adapters must implement. This enables swapping implementations without changing core logic.

IssueTrackerPort ​

Interface for issue tracker integrations.

python
from abc import ABC, abstractmethod
from spectryn.core.domain import IssueData, SubtaskData, Status

class IssueTrackerPort(ABC):
    """Abstract interface for issue trackers."""
    
    @property
    @abstractmethod
    def name(self) -> str:
        """Return the tracker name (e.g., 'Jira', 'GitHub')."""
        pass
    
    @abstractmethod
    def get_epic_children(self, epic_key: str) -> list[IssueData]:
        """
        Get all issues under an epic.
        
        Args:
            epic_key: The epic identifier
            
        Returns:
            List of issues linked to the epic
        """
        pass
    
    @abstractmethod
    def get_issue(self, issue_key: str) -> IssueData:
        """
        Get a single issue by key.
        
        Args:
            issue_key: The issue identifier
            
        Returns:
            Issue data
            
        Raises:
            IssueNotFoundError: If issue doesn't exist
        """
        pass
    
    @abstractmethod
    def update_description(
        self, 
        issue_key: str, 
        description: str
    ) -> None:
        """
        Update an issue's description.
        
        Args:
            issue_key: The issue identifier
            description: New description content
        """
        pass
    
    @abstractmethod
    def create_subtask(
        self, 
        parent_key: str, 
        subtask: SubtaskData
    ) -> str:
        """
        Create a subtask under a parent issue.
        
        Args:
            parent_key: Parent issue identifier
            subtask: Subtask data
            
        Returns:
            Created subtask key
        """
        pass
    
    @abstractmethod
    def transition_issue(
        self, 
        issue_key: str, 
        status: Status
    ) -> None:
        """
        Transition an issue to a new status.
        
        Args:
            issue_key: The issue identifier
            status: Target status
        """
        pass
    
    @abstractmethod
    def add_comment(
        self, 
        issue_key: str, 
        body: str
    ) -> str:
        """
        Add a comment to an issue.
        
        Args:
            issue_key: The issue identifier
            body: Comment body
            
        Returns:
            Created comment ID
        """
        pass

DocumentParserPort ​

Interface for document parsers.

python
from abc import ABC, abstractmethod
from spectryn.core.domain import Epic

class DocumentParserPort(ABC):
    """Abstract interface for document parsers."""
    
    @property
    @abstractmethod
    def supported_extensions(self) -> list[str]:
        """Return supported file extensions (e.g., ['.md', '.markdown'])."""
        pass
    
    @abstractmethod
    def parse(self, content: str) -> Epic:
        """
        Parse document content into an Epic.
        
        Args:
            content: Raw document content
            
        Returns:
            Parsed Epic with stories
            
        Raises:
            ParseError: If content is invalid
        """
        pass
    
    @abstractmethod
    def validate(self, content: str) -> list[ValidationError]:
        """
        Validate document without parsing.
        
        Args:
            content: Raw document content
            
        Returns:
            List of validation errors (empty if valid)
        """
        pass

DocumentFormatterPort ​

Interface for output formatters.

python
from abc import ABC, abstractmethod

class DocumentFormatterPort(ABC):
    """Abstract interface for output formatters."""
    
    @property
    @abstractmethod
    def name(self) -> str:
        """Return formatter name (e.g., 'ADF', 'HTML')."""
        pass
    
    @abstractmethod
    def format_description(self, description: Description) -> Any:
        """
        Format a description for the target system.
        
        Args:
            description: Structured description
            
        Returns:
            Formatted output (type depends on target)
        """
        pass

ConfigProviderPort ​

Interface for configuration providers.

python
from abc import ABC, abstractmethod
from typing import Any

class ConfigProviderPort(ABC):
    """Abstract interface for configuration providers."""
    
    @abstractmethod
    def get(self, key: str, default: Any = None) -> Any:
        """
        Get a configuration value.
        
        Args:
            key: Configuration key (dot-notation supported)
            default: Default value if not found
            
        Returns:
            Configuration value
        """
        pass
    
    @abstractmethod
    def get_required(self, key: str) -> Any:
        """
        Get a required configuration value.
        
        Args:
            key: Configuration key
            
        Returns:
            Configuration value
            
        Raises:
            ConfigurationError: If key not found
        """
        pass

Adapters (Implementations) ​

JiraAdapter ​

Jira implementation of IssueTrackerPort.

python
from spectryn.adapters.trackers.jira import JiraAdapter

# Create adapter with credentials
adapter = JiraAdapter(
    url="https://company.atlassian.net",
    email="user@company.com",
    api_token="token",
)

# Use the adapter
issues = adapter.get_epic_children("PROJ-123")
adapter.update_description("PROJ-456", "New description")
adapter.transition_issue("PROJ-456", Status.DONE)

MarkdownParser ​

Markdown implementation of DocumentParserPort.

python
from spectryn.adapters.input.parsers import MarkdownParser

parser = MarkdownParser()

# Check supported extensions
print(parser.supported_extensions)  # ['.md', '.markdown']

# Parse content
with open("EPIC.md") as f:
    epic = parser.parse(f.read())

# Validate without parsing
errors = parser.validate(content)
for error in errors:
    print(f"{error.line}: {error.message}")

ADFFormatter ​

Atlassian Document Format implementation.

python
from spectryn.adapters.output.formatters import ADFFormatter

formatter = ADFFormatter()

# Format description for Jira
adf_content = formatter.format_description(story.description)
# Returns ADF JSON structure

EnvironmentConfigProvider ​

Environment-based configuration.

python
from spectryn.adapters.infrastructure.config import EnvironmentConfigProvider

config = EnvironmentConfigProvider()

# Get values (checks env vars and .env file)
jira_url = config.get("jira.url")
verbose = config.get("sync.verbose", False)

# Get required value (raises if missing)
api_token = config.get_required("jira.api_token")

Creating Custom Adapters ​

Example: Linear Adapter ​

python
from linear_api import LinearClient
from spectryn.core.ports import IssueTrackerPort
from spectryn.core.domain import IssueData, Status

class LinearAdapter(IssueTrackerPort):
    """Linear.app adapter for spectryn."""
    
    def __init__(self, api_key: str):
        self.client = LinearClient(api_key=api_key)
    
    @property
    def name(self) -> str:
        return "Linear"
    
    def get_epic_children(self, epic_key: str) -> list[IssueData]:
        # epic_key is a Linear project ID
        project = self.client.get_project(epic_key)
        issues = project.issues()
        
        return [
            IssueData(
                key=issue.identifier,
                title=issue.title,
                description=issue.description,
                status=self._map_status(issue.state.name),
            )
            for issue in issues
        ]
    
    def _map_status(self, linear_state: str) -> Status:
        mapping = {
            "Backlog": Status.PLANNED,
            "Todo": Status.PLANNED,
            "In Progress": Status.IN_PROGRESS,
            "Done": Status.DONE,
            "Canceled": Status.DONE,
        }
        return mapping.get(linear_state, Status.PLANNED)
    
    def update_description(self, issue_key: str, description: str) -> None:
        self.client.update_issue(issue_key, description=description)
    
    def create_subtask(self, parent_key: str, subtask: SubtaskData) -> str:
        issue = self.client.create_issue(
            title=subtask.title,
            description=subtask.description,
            parent_id=parent_key,
        )
        return issue.identifier
    
    def transition_issue(self, issue_key: str, status: Status) -> None:
        state_name = {
            Status.PLANNED: "Backlog",
            Status.IN_PROGRESS: "In Progress",
            Status.DONE: "Done",
        }[status]
        
        state = self.client.get_state_by_name(state_name)
        self.client.update_issue(issue_key, state_id=state.id)
    
    def add_comment(self, issue_key: str, body: str) -> str:
        comment = self.client.create_comment(issue_key, body=body)
        return comment.id

Register the Adapter ​

python
from spectryn.plugins import get_registry

registry = get_registry()
registry.register_adapter("linear", LinearAdapter)

Use via CLI ​

bash
spectryn --tracker linear --markdown EPIC.md --epic proj_123

Released under the MIT License.