Coverage for src/qdrant_loader/connectors/jira/cloud_connector.py: 93%
56 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""Jira connector implementation."""
3from collections.abc import AsyncGenerator
4from datetime import datetime
5from urllib.parse import urlparse # noqa: F401 - may be used in URL handling
7from requests.auth import HTTPBasicAuth # noqa: F401 - compatibility
9from qdrant_loader.connectors.jira.connector import BaseJiraConnector
10from qdrant_loader.connectors.jira.models import (
11 JiraIssue,
12)
13from qdrant_loader.utils.logging import LoggingConfig
15logger = LoggingConfig.get_logger(__name__)
18class JiraCloudConnector(BaseJiraConnector):
19 """Jira cloud connector for fetching and processing issues."""
21 CLOUD_JIRA_VERSION = "3"
22 SEARCH_ENDPOINT = "search/jql"
24 def _get_api_url(self, endpoint: str) -> str:
25 """Construct the full API URL for an endpoint.
27 Args:
28 endpoint: API endpoint path
30 Returns:
31 str: Full API URL
32 """
34 return f"{self.base_url}/rest/api/{self.CLOUD_JIRA_VERSION}/{endpoint}"
36 async def get_issues(
37 self, updated_after: datetime | None = None
38 ) -> AsyncGenerator[JiraIssue, None]:
39 """
40 Get all issues from Jira.
42 Args:
43 updated_after: Optional datetime to filter issues updated after this time
45 Yields:
46 JiraIssue objects
47 """
48 # Resume from checkpoint if provided (WS-2 feature)
49 next_page_token: str | None = self._checkpoint_cursor
50 processed_count = 0
51 page_size = self.config.page_size
52 attempted_count = 0
53 # Log progress every 100 issues instead of every 50
54 progress_log_interval = 100
56 if next_page_token:
57 logger.info(
58 "🎫 Resuming JIRA issue retrieval from checkpoint",
59 project_key=self.config.project_key,
60 page_size=page_size,
61 next_page_token=next_page_token,
62 )
63 else:
64 logger.info(
65 "🎫 Starting JIRA issue retrieval",
66 project_key=self.config.project_key,
67 page_size=page_size,
68 updated_after=updated_after.isoformat() if updated_after else None,
69 )
71 while True:
72 jql = self._build_jql_filter(updated_after)
74 params = {
75 "jql": jql,
76 "maxResults": page_size,
77 "expand": "changelog",
78 "fields": "*all",
79 }
81 if next_page_token:
82 params["nextPageToken"] = next_page_token
84 logger.debug(
85 "Fetching JIRA issues page",
86 next_page_token=next_page_token,
87 page_size=page_size,
88 jql=jql,
89 )
91 try:
92 response = await self._make_request(
93 "GET", self.SEARCH_ENDPOINT, params=params
94 )
95 except Exception as e:
96 logger.error(
97 "Failed to fetch JIRA issues page",
98 next_page_token=next_page_token,
99 page_size=page_size,
100 error=str(e),
101 error_type=type(e).__name__,
102 )
103 raise
105 if not response or not response.get("issues"):
106 logger.debug(
107 "No more JIRA issues found, stopping pagination",
108 next_page_token=next_page_token,
109 total_processed=processed_count,
110 )
111 break
113 issues = response["issues"]
114 # Token that will be used to fetch the next page; save with documents
115 page_next_token = response.get("nextPageToken")
117 for issue in issues:
118 try:
119 parsed_issue = self._parse_issue(issue, self.config.extra_fields)
120 # Attach checkpoint info to the issue so downstream mapping
121 # can include it in Document metadata for checkpoint saving.
122 # Only attach checkpoint if there's a next page to resume from
123 if page_next_token:
124 parsed_issue.ingestion_checkpoint = {
125 "cursor_kind": "page_token",
126 "cursor_value": page_next_token,
127 "batch_index": 0,
128 }
129 yield parsed_issue
130 processed_count += 1
132 if (processed_count) % progress_log_interval == 0:
133 logger.info(
134 f"🎫 Processed {processed_count} JIRA issues so far"
135 )
137 except Exception as e:
138 logger.error(
139 "Failed to parse JIRA issue",
140 issue_id=issue.get("id"),
141 issue_key=issue.get("key"),
142 error=str(e),
143 error_type=type(e).__name__,
144 )
145 # Continue processing other issues instead of failing completely
146 continue
148 attempted_count += len(issues)
149 # Check next page token and save it for checkpoint
150 next_page_token = response.get("nextPageToken")
151 is_last = response.get("isLast")
153 if is_last or not next_page_token:
154 logger.info(
155 f"✅ Completed JIRA issue retrieval: "
156 f"{attempted_count} issues attempted, "
157 f"{processed_count} successfully processed"
158 )
159 break