How to use just one task tracker without lock-in; well sorta

How to use just one task tracker without lock-in; well sorta

Table of Contents

So I am a huge shill for Linear (linear.app), but I don’t want to keep my personal tasks in it:

  1. they’re sometimes private
  2. i dont want to lose them if my employement status with the company changes

There’s not a way to deal with this particularly nicely. So what’s a boy to do?

Behold! I attempt to sync them. Hopefully you’ll find it useful.

import things
import requests
import pandas as pd
import os
import argparse
import logging
from rich.console import Console
from rich.table import Table

console = Console()

linear_api_token = os.getenv("LINEAR_API_KEY")
if not linear_api_token:
    console.print("Error: LINEAR_API_KEY environment variable not set.", style="red")
    exit(1)

linear_team_id = '175xxxxxxxxxxxxxxxxxx' # PK Personal Team
linear_project_id = 'None'
linear_api_url = 'https://api.linear.app/graphql'

headers = {
    'Authorization': f'{linear_api_token}',
    'Content-Type': 'application/json',
}

def create_linear_ticket(
    linear_api_key,
    team_id,
    issue_title,
    issue_description,
):

    mutation = """
    mutation CreateIssue($teamId: String!, $title: String!, $description: String) {
        issueCreate(input: {teamId: $teamId, title: $title, description: $description}) {
            issue {
                id
                title
                description
                url
            }
        }
    }
    """

    variables = {
        "teamId": team_id,
        "title": issue_title,
        "description": issue_description,
    }


    response = requests.post(
        linear_api_url,
        json={
            "query": mutation,
            "variables": variables,
        },
        headers=headers,
    )

    if response.status_code == 200:
        response_data = response.json()
        print(response_data)
        parent_issue_id = response_data["data"]["issueCreate"]["issue"]["id"]
        issue_url = response_data["data"]["issueCreate"]["issue"]["url"]
        logging.info(f"Created ticket: {issue_url}")

        return issue_url, parent_issue_id
    else:
        logging.error(f"Failed to create parent issue. Status code: {response.status_code}, Response: {response.text}")
        return None

def get_things_tasks():
    today_tasks = things.today()
    selected_tasks = [
        task for task in today_tasks 
        if task.get('area_title') in ['In Progress', 'On Deck', 'Backlog']
    ]
    return selected_tasks

def create_linear_tasks(tasks, debug=False):
    for task in tasks:
        task_title = task['title']
        task_notes = task.get('notes', '')

        if debug:
            console.print(f'[DEBUG] Task to be created in Linear: {task_title}, Notes: {task_notes}', style="yellow")
        else:
            result = create_linear_ticket(
                linear_api_token,
                linear_team_id,
                task_title,
                task_notes,
            )

            if result:
                issue_url, parent_issue_id = result
                console.print(f'Successfully created task: {task_title}', style="green")
                console.print(f'Task URL: {issue_url}', style="blue")
            else:
                console.print(f'Failed to create task: {task_title}', style="red")

def extract_linear_tasks():
    query = {
        'query': '''
            query {
                issues(filter: {team: {id: {eq: "%s"}}}) {
                    nodes {
                        title
                        description
                        state {
                            name
                        }
                    }
                }
            }
        ''' % linear_team_id
    }

    response = requests.post(linear_api_url, headers=headers, json=query)
    
    if response.status_code == 200:
        issues = response.json()['data']['issues']['nodes']
        return issues
    else:
        console.print(f'Failed to fetch tasks from Linear. Status code: {response.status_code}, Response: {response.text}', style="red")
        return []

def save_tasks_to_csv(tasks, filename='linear_tasks.csv'):
    task_data = []
    for task in tasks:
        task_data.append({
            'title': task['title'],
            'notes': f"Imported from Linear - {task.get('description', '')}",
            'state': task['state']['name']
        })
    df = pd.DataFrame(task_data)
    df.to_csv(filename, index=False)
    console.print(f'Tasks saved to {filename}', style="blue")

def fetch_linear_teams():
    query = '''
        query Teams {
            teams {
                nodes {
                    id
                    name
                }
            }
        }
    '''

    response = requests.post(linear_api_url, headers=headers, json={'query': query})

    if response.status_code == 200:
        teams = response.json()['data']['teams']['nodes']
        table = Table(title="Linear Teams")
        table.add_column("ID", justify="right", style="cyan")
        table.add_column("Name", style="magenta")
        for team in teams:
            table.add_row(team['id'], team['name'])
        console.print(table)
    else:
        console.print(f'Failed to fetch teams from Linear. Status code: {response.status_code}, Response: {response.text}', style="red")


def main(target, debug):
    if target == 'linear':
        things_tasks = get_things_tasks()
        console.print(f'Found tasks: {things_tasks}', style="green")
        create_linear_tasks(things_tasks, debug)
    elif target == 'things':
        linear_tasks = extract_linear_tasks()
        save_tasks_to_csv(linear_tasks)
    
    console.print("Sync completed successfully.", style="bold green")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Sync tasks between Things3 and Linear")
    parser.add_argument('--target', choices=['linear', 'things'], help="Specify the target system: 'linear' to sync from Things3 to Linear, 'things' to sync from Linear to Things3")
    parser.add_argument('--debug', action='store_true', help="Enable debug mode to only show tasks without creating them in the target system")
    parser.add_argument('--fetch-teams', action='store_true', help="Fetch and display Linear teams")
    args = parser.parse_args()

    if args.fetch_teams:
        fetch_linear_teams()
    else:
        if not args.target:
            parser.error("The --target argument is required if not using --fetch-teams")
        main(args.target, args.debug)

Related Posts

Harnessing High Energy: Tips for Staying Productive

Harnessing High Energy: Tips for Staying Productive

I met a young entrepreneur who was struggling with some of the challenges I did when I was his age.

Read More
llm - for command line LLM Prototyping

llm - for command line LLM Prototyping

I have been doing a lot of exploration and prototyping for “The Intelligent Enterprise” (tm mine obvi) using LLMs.

Read More
read my blog posts to me, please? now with command line browser cleanup action!

read my blog posts to me, please? now with command line browser cleanup action!

Sometimes I’m running short on time, and I’d rather listen to something while I’m doing drivel work than read it.

Read More

Get new posts via email

Intuit Mailchimp

Copyright 2024-infinity, Paul Pereyda Karayan. Design by Zeon Studio