janet-cli 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
janet/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Janet AI CLI - Sync tickets to local markdown files."""
2
+
3
+ __version__ = "0.2.2"
janet/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for running janet as a module: python -m janet"""
2
+
3
+ from janet.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
janet/api/__init__.py ADDED
File without changes
janet/api/client.py ADDED
@@ -0,0 +1,128 @@
1
+ """Base API client with authentication headers."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from janet.auth.token_manager import TokenManager
8
+ from janet.config.manager import ConfigManager
9
+ from janet.utils.errors import NetworkError, AuthenticationError, TokenExpiredError
10
+
11
+
12
+ class APIClient:
13
+ """Base API client for Janet AI."""
14
+
15
+ def __init__(self, config_manager: ConfigManager):
16
+ """
17
+ Initialize API client.
18
+
19
+ Args:
20
+ config_manager: Configuration manager instance
21
+ """
22
+ self.config_manager = config_manager
23
+ self.token_manager = TokenManager(config_manager)
24
+ self.config = config_manager.get()
25
+ self.base_url = self.config.api.base_url
26
+ self.timeout = self.config.api.timeout
27
+
28
+ def _get_headers(self, include_org: bool = False) -> Dict[str, str]:
29
+ """
30
+ Get request headers with authentication.
31
+
32
+ Args:
33
+ include_org: Whether to include organization ID header
34
+
35
+ Returns:
36
+ Dictionary of headers
37
+ """
38
+ try:
39
+ access_token = self.token_manager.get_access_token()
40
+ except TokenExpiredError:
41
+ # Try to refresh token
42
+ from janet.auth.oauth_flow import OAuthFlow
43
+
44
+ oauth_flow = OAuthFlow(self.config_manager)
45
+ try:
46
+ oauth_flow.refresh_token()
47
+ access_token = self.token_manager.get_access_token()
48
+ except Exception:
49
+ raise AuthenticationError(
50
+ "Token expired and refresh failed. Please log in again."
51
+ )
52
+
53
+ headers = {
54
+ "Authorization": f"Bearer {access_token}",
55
+ "Content-Type": "application/json",
56
+ }
57
+
58
+ if include_org and self.config.selected_organization:
59
+ headers["X-Organization-ID"] = self.config.selected_organization.id
60
+
61
+ return headers
62
+
63
+ def get(self, endpoint: str, include_org: bool = False, **kwargs) -> Dict:
64
+ """
65
+ Make GET request.
66
+
67
+ Args:
68
+ endpoint: API endpoint (relative to base_url)
69
+ include_org: Whether to include organization ID header
70
+ **kwargs: Additional arguments for httpx.get
71
+
72
+ Returns:
73
+ Response JSON
74
+
75
+ Raises:
76
+ NetworkError: If request fails
77
+ """
78
+ url = f"{self.base_url}{endpoint}"
79
+ headers = self._get_headers(include_org=include_org)
80
+
81
+ try:
82
+ response = httpx.get(url, headers=headers, timeout=self.timeout, **kwargs)
83
+ response.raise_for_status()
84
+ return response.json()
85
+ except httpx.HTTPStatusError as e:
86
+ if e.response.status_code == 401:
87
+ raise AuthenticationError("Authentication failed. Please log in again.")
88
+ raise NetworkError(f"API request failed: {e.response.status_code} {e.response.text}")
89
+ except httpx.TimeoutException:
90
+ raise NetworkError(f"Request timeout after {self.timeout}s")
91
+ except Exception as e:
92
+ raise NetworkError(f"Network error: {e}")
93
+
94
+ def post(
95
+ self, endpoint: str, data: Optional[Dict] = None, include_org: bool = False, **kwargs
96
+ ) -> Dict:
97
+ """
98
+ Make POST request.
99
+
100
+ Args:
101
+ endpoint: API endpoint (relative to base_url)
102
+ data: Request body data
103
+ include_org: Whether to include organization ID header
104
+ **kwargs: Additional arguments for httpx.post
105
+
106
+ Returns:
107
+ Response JSON
108
+
109
+ Raises:
110
+ NetworkError: If request fails
111
+ """
112
+ url = f"{self.base_url}{endpoint}"
113
+ headers = self._get_headers(include_org=include_org)
114
+
115
+ try:
116
+ response = httpx.post(
117
+ url, headers=headers, json=data, timeout=self.timeout, **kwargs
118
+ )
119
+ response.raise_for_status()
120
+ return response.json()
121
+ except httpx.HTTPStatusError as e:
122
+ if e.response.status_code == 401:
123
+ raise AuthenticationError("Authentication failed. Please log in again.")
124
+ raise NetworkError(f"API request failed: {e.response.status_code} {e.response.text}")
125
+ except httpx.TimeoutException:
126
+ raise NetworkError(f"Request timeout after {self.timeout}s")
127
+ except Exception as e:
128
+ raise NetworkError(f"Network error: {e}")
janet/api/models.py ADDED
@@ -0,0 +1,92 @@
1
+ """Pydantic models for API responses."""
2
+
3
+ from typing import List, Optional
4
+ from datetime import datetime
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class Comment(BaseModel):
9
+ """Ticket comment model."""
10
+
11
+ id: str
12
+ content: str
13
+ created_by: str
14
+ created_at: str
15
+ updated_at: str
16
+
17
+
18
+ class Attachment(BaseModel):
19
+ """Ticket attachment model."""
20
+
21
+ id: str
22
+ mapping_id: str
23
+ original_filename: str
24
+ mime_type: str
25
+ file_size_bytes: int
26
+ gcs_uri: str
27
+ signed_url: Optional[str] = None
28
+ ai_description: Optional[str] = None
29
+ ai_processing_status: str
30
+ uploaded_by: str
31
+ created_at: str
32
+ is_direct: bool
33
+
34
+
35
+ class ChildTask(BaseModel):
36
+ """Child task model."""
37
+
38
+ id: str
39
+ childIdentifier: str
40
+ fullIdentifier: str
41
+ title: str
42
+ description: Optional[str] = None
43
+ status: Optional[str] = None
44
+ priority: Optional[str] = None
45
+ assignee: Optional[str] = None
46
+ displayOrder: str
47
+ createdBy: str
48
+ updatedBy: Optional[str] = None
49
+ createdAt: str
50
+ updatedAt: str
51
+
52
+
53
+ class Ticket(BaseModel):
54
+ """Ticket model."""
55
+
56
+ id: str
57
+ ticket_identifier: str
58
+ ticket_key: str
59
+ title: str
60
+ description: Optional[str] = None
61
+ description_yjs_binary: Optional[str] = None
62
+ ai_summary: Optional[str] = None
63
+ ticket_evaluation: Optional[str] = None
64
+ status: str
65
+ priority: str
66
+ issue_type: str
67
+ assignees: List[str] = []
68
+ labels: List[str] = []
69
+ story_points: Optional[str] = None
70
+ due_date: Optional[str] = None
71
+ sprint: Optional[str] = None
72
+ creator: str
73
+ project_id: str
74
+ organization_id: str
75
+ created_at: str
76
+ updated_at: str
77
+ comments: List[Comment] = []
78
+ child_tasks: List[ChildTask] = []
79
+
80
+
81
+ class OrganizationMember(BaseModel):
82
+ """Organization member model."""
83
+
84
+ id: str
85
+ userId: str
86
+ email: str
87
+ firstName: Optional[str] = None
88
+ lastName: Optional[str] = None
89
+ profilePictureUrl: Optional[str] = None
90
+ role: str
91
+ status: str
92
+ joinedAt: str
@@ -0,0 +1,57 @@
1
+ """Organization API methods."""
2
+
3
+ from typing import List, Dict
4
+
5
+ from janet.api.client import APIClient
6
+ from janet.config.manager import ConfigManager
7
+
8
+
9
+ class OrganizationAPI(APIClient):
10
+ """API methods for organization management."""
11
+
12
+ def __init__(self, config_manager: ConfigManager):
13
+ """
14
+ Initialize organization API.
15
+
16
+ Args:
17
+ config_manager: Configuration manager instance
18
+ """
19
+ super().__init__(config_manager)
20
+
21
+ def list_organizations(self) -> List[Dict]:
22
+ """
23
+ List available organizations for the authenticated user.
24
+
25
+ Returns:
26
+ List of organization dictionaries
27
+
28
+ Raises:
29
+ NetworkError: If API request fails
30
+ AuthenticationError: If not authenticated
31
+ """
32
+ response = self.get("/api/v1/organizations/list")
33
+
34
+ if not response.get("success"):
35
+ raise Exception(response.get("error", "Failed to fetch organizations"))
36
+
37
+ return response.get("organizations", [])
38
+
39
+ def get_organization(self, org_id: str) -> Dict:
40
+ """
41
+ Get organization details.
42
+
43
+ Args:
44
+ org_id: Organization ID
45
+
46
+ Returns:
47
+ Organization dictionary
48
+
49
+ Raises:
50
+ NetworkError: If API request fails
51
+ """
52
+ response = self.get(f"/api/v1/organizations/{org_id}")
53
+
54
+ if not response.get("success"):
55
+ raise Exception(response.get("error", "Failed to fetch organization"))
56
+
57
+ return response.get("organization", {})
janet/api/projects.py ADDED
@@ -0,0 +1,57 @@
1
+ """Project API methods."""
2
+
3
+ from typing import List, Dict
4
+
5
+ from janet.api.client import APIClient
6
+ from janet.config.manager import ConfigManager
7
+
8
+
9
+ class ProjectAPI(APIClient):
10
+ """API methods for project management."""
11
+
12
+ def __init__(self, config_manager: ConfigManager):
13
+ """
14
+ Initialize project API.
15
+
16
+ Args:
17
+ config_manager: Configuration manager instance
18
+ """
19
+ super().__init__(config_manager)
20
+
21
+ def list_projects(self) -> List[Dict]:
22
+ """
23
+ List projects in the current organization.
24
+
25
+ Returns:
26
+ List of project dictionaries
27
+
28
+ Raises:
29
+ NetworkError: If API request fails
30
+ AuthenticationError: If not authenticated
31
+ """
32
+ response = self.get("/api/v1/projects", include_org=True)
33
+
34
+ if not response.get("success"):
35
+ raise Exception(response.get("error", "Failed to fetch projects"))
36
+
37
+ return response.get("projects", [])
38
+
39
+ def get_project(self, project_id: str) -> Dict:
40
+ """
41
+ Get project details.
42
+
43
+ Args:
44
+ project_id: Project ID
45
+
46
+ Returns:
47
+ Project dictionary
48
+
49
+ Raises:
50
+ NetworkError: If API request fails
51
+ """
52
+ response = self.get(f"/api/v1/projects/{project_id}", include_org=True)
53
+
54
+ if not response.get("success"):
55
+ raise Exception(response.get("error", "Failed to fetch project"))
56
+
57
+ return response.get("project", {})
janet/api/tickets.py ADDED
@@ -0,0 +1,125 @@
1
+ """Ticket API methods."""
2
+
3
+ from typing import List, Dict, Optional
4
+
5
+ from janet.api.client import APIClient
6
+ from janet.config.manager import ConfigManager
7
+
8
+
9
+ class TicketAPI(APIClient):
10
+ """API methods for ticket management."""
11
+
12
+ def __init__(self, config_manager: ConfigManager):
13
+ """
14
+ Initialize ticket API.
15
+
16
+ Args:
17
+ config_manager: Configuration manager instance
18
+ """
19
+ super().__init__(config_manager)
20
+
21
+ def list_tickets(
22
+ self,
23
+ project_id: str,
24
+ limit: int = 1000,
25
+ offset: int = 0,
26
+ show_resolved: bool = True,
27
+ ) -> Dict:
28
+ """
29
+ List tickets for a project.
30
+
31
+ Args:
32
+ project_id: Project ID
33
+ limit: Maximum tickets to return
34
+ offset: Pagination offset
35
+ show_resolved: Include resolved tickets older than 7 days
36
+
37
+ Returns:
38
+ Dictionary with tickets and metadata
39
+
40
+ Raises:
41
+ NetworkError: If API request fails
42
+ """
43
+ endpoint = f"/api/v1/projects/{project_id}/tickets/list"
44
+
45
+ data = {
46
+ "limit": limit,
47
+ "offset": offset,
48
+ "show_resolved_over_7_days": show_resolved,
49
+ }
50
+
51
+ response = self.post(endpoint, data=data, include_org=True)
52
+
53
+ if not response.get("success"):
54
+ raise Exception(response.get("error", "Failed to list tickets"))
55
+
56
+ return response
57
+
58
+ def get_ticket(self, ticket_id: str) -> Dict:
59
+ """
60
+ Get full ticket details.
61
+
62
+ Args:
63
+ ticket_id: Ticket ID
64
+
65
+ Returns:
66
+ Ticket dictionary with all fields
67
+
68
+ Raises:
69
+ NetworkError: If API request fails
70
+ """
71
+ response = self.get(f"/api/v1/tickets/{ticket_id}", include_org=True)
72
+
73
+ if not response.get("success"):
74
+ raise Exception(response.get("error", "Failed to fetch ticket"))
75
+
76
+ return response.get("ticket", {})
77
+
78
+ def batch_fetch(self, ticket_ids: List[str]) -> List[Dict]:
79
+ """
80
+ Fetch multiple tickets in one request.
81
+
82
+ Args:
83
+ ticket_ids: List of ticket IDs
84
+
85
+ Returns:
86
+ List of ticket dictionaries
87
+
88
+ Raises:
89
+ NetworkError: If API request fails
90
+ """
91
+ if not ticket_ids:
92
+ return []
93
+
94
+ data = {"ticket_ids": ticket_ids}
95
+ response = self.post("/api/v1/tickets/batch", data=data, include_org=True)
96
+
97
+ if not response.get("success"):
98
+ raise Exception(response.get("error", "Failed to batch fetch tickets"))
99
+
100
+ return response.get("tickets", [])
101
+
102
+ def get_ticket_attachments(self, ticket_id: str) -> Dict:
103
+ """
104
+ Get attachments for a ticket.
105
+
106
+ Args:
107
+ ticket_id: Ticket ID
108
+
109
+ Returns:
110
+ Dictionary with direct and indirect attachments
111
+
112
+ Raises:
113
+ NetworkError: If API request fails
114
+ """
115
+ response = self.get(
116
+ f"/api/v1/tickets/{ticket_id}/attachments", include_org=True
117
+ )
118
+
119
+ if not response.get("success"):
120
+ raise Exception(response.get("error", "Failed to fetch attachments"))
121
+
122
+ return {
123
+ "direct_attachments": response.get("direct_attachments", []),
124
+ "indirect_attachments": response.get("indirect_attachments", []),
125
+ }
janet/auth/__init__.py ADDED
File without changes