Files
markitect-main/services/export_service.py
tegwick 7f5309c4b0 refactor: Separate CLI presentation from core business logic
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>
2025-09-26 15:08:54 +02:00

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