Spaces:
Running
Running
| """ | |
| 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 | |