FocusFlowAI / linear_client.py
avaliev's picture
Initial commit
0491e54 verified
"""
Linear Client for FocusFlow.
Handles integration with Linear API (or MCP server) for task synchronization.
Falls back to mock data if no API key is provided.
"""
import os
import json
import requests
from typing import List, Dict, Optional
from datetime import datetime
class LinearClient:
"""Client for interacting with Linear."""
def __init__(self, api_key: Optional[str] = None):
"""Initialize Linear client."""
self.api_key = api_key or os.getenv("LINEAR_API_KEY")
self.api_url = "https://api.linear.app/graphql"
self.is_active = bool(self.api_key)
if not self.is_active:
print("ℹ️ Linear: No API key found. Using mock data.")
def _headers(self) -> Dict[str, str]:
"""Get request headers."""
return {
"Content-Type": "application/json",
"Authorization": self.api_key
}
def _query(self, query: str, variables: Dict = None) -> Dict:
"""Execute GraphQL query."""
if not self.is_active:
return {}
try:
response = requests.post(
self.api_url,
headers=self._headers(),
json={"query": query, "variables": variables or {}}
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"⚠️ Linear API error: {e}")
return {}
def get_user_projects(self) -> List[Dict]:
"""Get projects for the current user."""
if not self.is_active:
return [
{"id": "mock-1", "name": "Website Redesign", "description": "Overhaul the company website"},
{"id": "mock-2", "name": "Mobile App", "description": "iOS and Android app development"},
{"id": "mock-3", "name": "API Migration", "description": "Migrate legacy API to GraphQL"}
]
query = """
query {
viewer {
projects(first: 10) {
nodes {
id
name
description
}
}
}
}
"""
result = self._query(query)
try:
return result.get("data", {}).get("viewer", {}).get("projects", {}).get("nodes", [])
except Exception:
return []
def get_project_tasks(self, project_id: str) -> List[Dict]:
"""Get tasks for a specific project."""
if not self.is_active:
# Return mock tasks based on project ID
if project_id == "mock-1":
return [
{"id": "L-101", "title": "Design Homepage", "description": "Create Figma mockups", "estimate": 60},
{"id": "L-102", "title": "Implement Header", "description": "React component for header", "estimate": 30},
{"id": "L-103", "title": "Fix CSS Bugs", "description": "Fix mobile layout issues", "estimate": 45}
]
return [
{"id": "L-201", "title": "Setup Repo", "description": "Initialize git repository", "estimate": 15},
{"id": "L-202", "title": "Basic Auth", "description": "Implement login flow", "estimate": 60}
]
query = """
query($projectId: ID!) {
project(id: $projectId) {
issues(first: 20, filter: { state: { name: { neq: "Done" } } }) {
nodes {
id
title
description
estimate
}
}
}
}
"""
result = self._query(query, {"projectId": project_id})
try:
return result.get("data", {}).get("project", {}).get("issues", {}).get("nodes", [])
except Exception:
return []
def create_task(self, title: str, description: str = "", team_id: str = None) -> Optional[str]:
"""Create a new task (issue) in Linear."""
if not self.is_active:
print(f"ℹ️ Linear (Mock): Created task '{title}'")
return "mock-new-id"
# Note: This requires a team_id. For simplicity, we might need to fetch a default team first.
# This is a simplified implementation.
if not team_id:
# Try to get the first team
team_query = """query { viewer { teams(first: 1) { nodes { id } } } }"""
team_res = self._query(team_query)
try:
team_id = team_res["data"]["viewer"]["teams"]["nodes"][0]["id"]
except:
return None
mutation = """
mutation($title: String!, $description: String, $teamId: String!) {
issueCreate(input: { title: $title, description: $description, teamId: $teamId }) {
issue {
id
}
}
}
"""
result = self._query(mutation, {"title": title, "description": description, "teamId": team_id})
try:
return result["data"]["issueCreate"]["issue"]["id"]
except:
return None