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 +3 -0
- janet/__main__.py +6 -0
- janet/api/__init__.py +0 -0
- janet/api/client.py +128 -0
- janet/api/models.py +92 -0
- janet/api/organizations.py +57 -0
- janet/api/projects.py +57 -0
- janet/api/tickets.py +125 -0
- janet/auth/__init__.py +0 -0
- janet/auth/callback_server.py +360 -0
- janet/auth/oauth_flow.py +276 -0
- janet/auth/token_manager.py +92 -0
- janet/cli.py +602 -0
- janet/config/__init__.py +0 -0
- janet/config/manager.py +116 -0
- janet/config/models.py +66 -0
- janet/markdown/__init__.py +0 -0
- janet/markdown/generator.py +272 -0
- janet/markdown/yjs_converter.py +225 -0
- janet/sync/__init__.py +0 -0
- janet/sync/file_manager.py +199 -0
- janet/sync/readme_generator.py +174 -0
- janet/sync/sync_engine.py +271 -0
- janet/utils/__init__.py +0 -0
- janet/utils/console.py +39 -0
- janet/utils/errors.py +49 -0
- janet/utils/paths.py +66 -0
- janet_cli-0.2.2.dist-info/METADATA +220 -0
- janet_cli-0.2.2.dist-info/RECORD +33 -0
- janet_cli-0.2.2.dist-info/WHEEL +5 -0
- janet_cli-0.2.2.dist-info/entry_points.txt +2 -0
- janet_cli-0.2.2.dist-info/licenses/LICENSE +21 -0
- janet_cli-0.2.2.dist-info/top_level.txt +1 -0
janet/__init__.py
ADDED
janet/__main__.py
ADDED
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
|