"""
TickTick Task Importer - Import tasks from markdown files
"""
import os
from typing import List, Dict, Optional
from src.importers.markdown_parser import MarkdownTaskParser, MarkdownTask
from src.api.ticktick_api import TickTickAPI
from dotenv import load_dotenv
load_dotenv()
class TaskImporter:
"""Import markdown tasks into TickTick"""
def __init__(self, access_token: Optional[str] = None):
"""
Initialize importer
Args:
access_token: TickTick access token (optional, reads from env)
"""
self.access_token = access_token or os.getenv('TICKTICK_ACCESS_TOKEN')
self.api = None
self.projects = None
self.parser = MarkdownTaskParser()
if self.access_token:
self.api = TickTickAPI(self.access_token)
def authenticate(self) -> bool:
"""Check authentication status"""
if not self.access_token:
print("No access token found!")
print("Run 'python get_token.py' to authenticate")
return False
user = self.api.get_user_info()
if user:
print(f"Authenticated as: {user.get('username', 'User')}")
return True
else:
print("Authentication failed - token may be invalid")
return False
def get_projects(self, refresh: bool = False) -> List[Dict]:
"""Get all projects (cached)"""
if self.projects is None or refresh:
self.projects = self.api.get_projects()
return self.projects or []
def find_project_id(self, project_name: str) -> Optional[str]:
"""
Find project ID by name (case-insensitive)
Args:
project_name: Project name to search for
Returns:
Project ID or None if not found
"""
projects = self.get_projects()
for project in projects:
if project_name.lower() in project['name'].lower():
return project['id']
return None
def import_from_file(
self,
file_path: str,
default_project: Optional[str] = None,
dry_run: bool = False
) -> Dict:
"""
Import tasks from a markdown file
Args:
file_path: Path to markdown file
default_project: Default project name (if not specified in task)
dry_run: If True, parse but don't create tasks
Returns:
Import summary dictionary
"""
if not self.api:
return {'error': 'Not authenticated'}
print(f"Parsing {file_path}...")
tasks = self.parser.parse_file(file_path)
if not tasks:
print("No tasks found in file")
return {'error': 'No tasks found'}
summary = self.parser.get_summary()
print(f"\nParsed {summary['total_tasks']} tasks:")
print(f" - {summary['completed_tasks']} completed")
print(f" - {summary['tasks_with_tags']} with tags")
print(f" - {summary['tasks_with_project']} with project specified")
if dry_run:
print("\nDRY RUN - No tasks will be created\n")
self._preview_tasks(tasks)
return {'dry_run': True, 'tasks': len(tasks)}
print(f"\nImporting {len(tasks)} tasks...\n")
results = {
'total': len(tasks),
'created': 0,
'failed': 0,
'skipped': 0,
'errors': []
}
for task in tasks:
result = self._import_task(task, default_project)
if result['status'] == 'created':
results['created'] += 1
print(f"Created: {task.title[:50]}...")
elif result['status'] == 'skipped':
results['skipped'] += 1
print(f"Skipped: {task.title[:50]}... ({result['reason']})")
else:
results['failed'] += 1
print(f"Failed: {task.title[:50]}... ({result['error']})")
results['errors'].append({
'task': task.title,
'error': result['error']
})
print(f"\n{'='*60}")
print(f"Import Summary")
print(f"{'='*60}")
print(f"Created: {results['created']}")
print(f"Skipped: {results['skipped']}")
print(f"Failed: {results['failed']}")
print(f"{'='*60}\n")
return results
def _import_task(
self,
task: MarkdownTask,
default_project: Optional[str]
) -> Dict:
"""
Import a single task
Args:
task: MarkdownTask object
default_project: Default project name
Returns:
Result dictionary with status and details
"""
if task.completed:
return {'status': 'skipped', 'reason': 'already completed'}
project_name = task.project or default_project
project_id = None
if project_name:
project_id = self.find_project_id(project_name)
if not project_id:
return {
'status': 'failed',
'error': f"Project '{project_name}' not found"
}
try:
result = self.api.create_task(
title=task.title,
content=task.to_dict()['content'],
project_id=project_id,
tags=task.tags if task.tags else None
)
if result:
return {'status': 'created', 'task_id': result.get('id')}
else:
return {'status': 'failed', 'error': 'API returned None'}
except Exception as e:
return {'status': 'failed', 'error': str(e)}
def _preview_tasks(self, tasks: List[MarkdownTask]):
"""Print preview of tasks to be imported"""
print("Tasks to import:")
print("="*60)
for i, task in enumerate(tasks, 1):
status = "[x]" if task.completed else "[ ]"
print(f"\n{i}. {status} {task.title}")
if task.tags:
print(f" Tags: {', '.join(task.tags)}")
if task.project:
print(f" Project: {task.project}")
if task.to_dict()['content']:
content_preview = task.to_dict()['content'][:100]
print(f" Description: {content_preview}...")
print("\n" + "="*60)
def main():
"""Interactive CLI for task importer"""
print("="*60)
print("TickTick Task Importer")
print("="*60)
importer = TaskImporter()
if not importer.authenticate():
return
print("\n" + "="*60)
print("Your Projects")
print("="*60)
projects = importer.get_projects()
for project in projects:
print(f" - {project['name']}")
print("\n" + "="*60)
print("Import Tasks")
print("="*60)
file_path = input("Enter markdown file path: ").strip()
if not os.path.exists(file_path):
print(f"File not found: {file_path}")
return
default_project = input("Default project (leave empty for Inbox): ").strip() or None
dry_run_choice = input("Preview only (dry run)? (y/n, default=n): ").strip().lower()
dry_run = (dry_run_choice == 'y')
if not dry_run:
print("\nThis will create tasks in your TickTick account!")
confirm = input("Continue? (y/n): ").strip().lower()
if confirm != 'y':
print("Import cancelled")
return
importer.import_from_file(file_path, default_project, dry_run)
if __name__ == "__main__":
main()