Complete architectural separation of concerns implementing clean layered design: • Services Layer: Pure business logic isolated from presentation - WorkspaceService: TDD workspace operations - IssueService: Issue management and creation - ProjectService: Project management and milestones - ExportService: Unix-friendly data export • CLI Layer: Clean presentation with command/presenter separation - Commands delegate to services for all business operations - Presenters handle formatted output and error messaging - Framework provides unified interface • Benefits: - Eliminates mixed concerns in 943-line CLI monolith - Enables easier testing and maintenance - Preserves all existing functionality and Unix pipeline compatibility - Provides foundation for future CLI development Resolves issue #20: CLI separation from core logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
"""
|
|
Export service - business logic for data export and formatting operations.
|
|
"""
|
|
|
|
import json
|
|
from typing import List, Dict, Any, Optional
|
|
from datetime import datetime
|
|
|
|
from gitea.models import Issue
|
|
from .issue_service import IssueService
|
|
|
|
|
|
class ExportService:
|
|
"""Service for export and data formatting operations."""
|
|
|
|
def __init__(self):
|
|
self.issue_service = IssueService()
|
|
|
|
def get_issues_data(self, state: str = "all",
|
|
sort_by: str = "number",
|
|
filter_state: Optional[str] = None,
|
|
filter_priority: Optional[str] = None,
|
|
include_state: bool = False) -> List[Dict[str, Any]]:
|
|
"""Get structured issue data for export.
|
|
|
|
Args:
|
|
state: Issue state filter (all, open, closed)
|
|
sort_by: Sort field (number, title, priority, state, created, updated)
|
|
filter_state: Additional state filter (open, closed)
|
|
filter_priority: Priority filter (low, medium, high, critical, none)
|
|
include_state: Whether to include detailed state information
|
|
|
|
Returns:
|
|
List of issue data dictionaries
|
|
"""
|
|
issues = self.issue_service.list_issues(state)
|
|
|
|
# Convert to structured data
|
|
issue_data = []
|
|
for issue in issues:
|
|
# Get priority and state from labels
|
|
priority = issue.priority or "none"
|
|
status = issue.status or "none"
|
|
|
|
issue_info = {
|
|
'number': issue.number,
|
|
'title': issue.title.replace('\t', ' ').replace('\n', ' '), # Clean for TSV
|
|
'priority': priority,
|
|
'state': issue.state, # open/closed from basic data
|
|
'status': status, # detailed status from labels
|
|
'created': issue.created_at.strftime('%Y-%m-%d'),
|
|
'updated': issue.updated_at.strftime('%Y-%m-%d')
|
|
}
|
|
issue_data.append(issue_info)
|
|
|
|
# Apply filters
|
|
if filter_state:
|
|
if filter_state == "open":
|
|
issue_data = [i for i in issue_data if i['state'] == 'open']
|
|
elif filter_state == "closed":
|
|
issue_data = [i for i in issue_data if i['state'] == 'closed']
|
|
|
|
if filter_priority:
|
|
issue_data = [i for i in issue_data if i['priority'] == filter_priority]
|
|
|
|
# Sort issues
|
|
sort_key_map = {
|
|
'number': lambda x: x['number'],
|
|
'title': lambda x: x['title'].lower(),
|
|
'priority': lambda x: {'critical': 4, 'high': 3, 'medium': 2, 'low': 1, 'none': 0}[x['priority']],
|
|
'state': lambda x: x['state'],
|
|
'created': lambda x: x['created'],
|
|
'updated': lambda x: x['updated']
|
|
}
|
|
|
|
if sort_by in sort_key_map:
|
|
issue_data.sort(key=sort_key_map[sort_by], reverse=(sort_by in ['number', 'priority', 'created', 'updated']))
|
|
|
|
return issue_data
|
|
|
|
def format_issues_tsv(self, issue_data: List[Dict[str, Any]], include_state: bool = False) -> str:
|
|
"""Format issues as TSV."""
|
|
lines = []
|
|
for issue in issue_data:
|
|
if include_state:
|
|
lines.append(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["state"]}\t{issue["created"]}\t{issue["updated"]}')
|
|
else:
|
|
lines.append(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["created"]}\t{issue["updated"]}')
|
|
return '\n'.join(lines)
|
|
|
|
def format_issues_csv(self, issue_data: List[Dict[str, Any]], include_state: bool = False) -> str:
|
|
"""Format issues as CSV."""
|
|
lines = []
|
|
|
|
# Header
|
|
if include_state:
|
|
lines.append("number,title,priority,state,created,updated")
|
|
for issue in issue_data:
|
|
title = issue['title'].replace('"', '""') # Escape quotes
|
|
lines.append(f'{issue["number"]},"{title}",{issue["priority"]},{issue["state"]},{issue["created"]},{issue["updated"]}')
|
|
else:
|
|
lines.append("number,title,priority,created,updated")
|
|
for issue in issue_data:
|
|
title = issue['title'].replace('"', '""')
|
|
lines.append(f'{issue["number"]},"{title}",{issue["priority"]},{issue["created"]},{issue["updated"]}')
|
|
|
|
return '\n'.join(lines)
|
|
|
|
def format_issues_json(self, issue_data: List[Dict[str, Any]]) -> str:
|
|
"""Format issues as JSON."""
|
|
return json.dumps(issue_data, indent=2)
|
|
|
|
def format_issues_fields(self, issue_data: List[Dict[str, Any]], include_state: bool = False) -> str:
|
|
"""Format issues as space-separated fields for awk processing."""
|
|
lines = []
|
|
|
|
# Header
|
|
if include_state:
|
|
lines.append("NUMBER TITLE PRIORITY STATE CREATED UPDATED")
|
|
for issue in issue_data:
|
|
title = issue['title'].replace(' ', '_')
|
|
lines.append(f'{issue["number"]} {title} {issue["priority"]} {issue["state"]} {issue["created"]} {issue["updated"]}')
|
|
else:
|
|
lines.append("NUMBER TITLE PRIORITY CREATED UPDATED")
|
|
for issue in issue_data:
|
|
title = issue['title'].replace(' ', '_')
|
|
lines.append(f'{issue["number"]} {title} {issue["priority"]} {issue["created"]} {issue["updated"]}')
|
|
|
|
return '\n'.join(lines)
|
|
|
|
def export_issues(self, format_type: str = "tsv", **kwargs) -> str:
|
|
"""Export issues in specified format.
|
|
|
|
Args:
|
|
format_type: Output format (tsv, csv, json, fields)
|
|
**kwargs: Export parameters (sort_by, filter_state, etc.)
|
|
|
|
Returns:
|
|
Formatted string output
|
|
"""
|
|
issue_data = self.get_issues_data(**kwargs)
|
|
|
|
if format_type == "json":
|
|
return self.format_issues_json(issue_data)
|
|
elif format_type == "csv":
|
|
return self.format_issues_csv(issue_data, kwargs.get('include_state', False))
|
|
elif format_type == "fields":
|
|
return self.format_issues_fields(issue_data, kwargs.get('include_state', False))
|
|
else: # Default TSV
|
|
return self.format_issues_tsv(issue_data, kwargs.get('include_state', False)) |