Coverage for src/qdrant_loader/connectors/jira/data_center_connector.py: 88%
57 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 JiraDataCenterConnector(BaseJiraConnector):
19 """Jira data center connector for fetching and processing issues."""
21 SEARCH_ENDPOINT = "search"
23 def _get_api_url(self, endpoint: str) -> str:
24 """Construct the full API URL for an endpoint.
26 Args:
27 endpoint: API endpoint path
29 Returns:
30 str: Full API URL
31 """
32 return f"{self.base_url}/rest/api/2/{endpoint}"
34 async def get_issues(
35 self, updated_after: datetime | None = None
36 ) -> AsyncGenerator[JiraIssue, None]:
37 """
38 Get all issues from Jira.
40 Args:
41 updated_after: Optional datetime to filter issues updated after this time
43 Yields:
44 JiraIssue objects
45 """
46 # Resume from checkpoint if provided (WS-2 feature)
47 # For DataCenter, checkpoint_cursor is the startAt offset as a string
48 try:
49 start_at = int(self._checkpoint_cursor) if self._checkpoint_cursor else 0
50 except (ValueError, TypeError):
51 start_at = 0
53 page_size = self.config.page_size
54 total_issues = 0
55 processed_count = 0
57 if self._checkpoint_cursor:
58 logger.info(
59 "🎫 Resuming JIRA issue retrieval from checkpoint",
60 project_key=self.config.project_key,
61 page_size=page_size,
62 start_at=start_at,
63 )
64 else:
65 logger.info(
66 "🎫 Starting JIRA issue retrieval",
67 project_key=self.config.project_key,
68 page_size=page_size,
69 updated_after=updated_after.isoformat() if updated_after else None,
70 )
72 while True:
73 jql = self._build_jql_filter(updated_after)
75 params = {
76 "jql": jql,
77 "startAt": start_at,
78 "maxResults": page_size,
79 "expand": "changelog",
80 "fields": "*all",
81 }
83 logger.debug(
84 "Fetching JIRA issues page",
85 start_at=start_at,
86 page_size=page_size,
87 jql=jql,
88 )
90 try:
91 response = await self._make_request(
92 "GET", self.SEARCH_ENDPOINT, params=params
93 )
94 except Exception as e:
95 logger.error(
96 "Failed to fetch JIRA issues page",
97 start_at=start_at,
98 page_size=page_size,
99 error=str(e),
100 error_type=type(e).__name__,
101 )
102 raise
104 if not response or not response.get("issues"):
105 logger.debug(
106 "No more JIRA issues found, stopping pagination",
107 start_at=start_at,
108 total_processed=start_at,
109 issues_processed=processed_count,
110 )
111 break
113 issues = response["issues"]
114 # Compute the offset for the next page and attach as checkpoint info
115 page_next_offset = str(start_at + len(issues))
117 # Update total count if not set
118 if total_issues == 0:
119 total_issues = response.get("total", 0)
120 logger.info(f"🎫 Found {total_issues} JIRA issues to process")
122 # Log progress every 100 issues instead of every 50
123 progress_log_interval = 100
125 for i, issue in enumerate(issues):
126 try:
127 parsed_issue = self._parse_issue(issue, self.config.extra_fields)
128 # Attach checkpoint info for this page
129 parsed_issue.ingestion_checkpoint = {
130 "cursor_kind": "jql_window",
131 "cursor_value": page_next_offset,
132 "batch_index": 0,
133 }
134 yield parsed_issue
135 processed_count += 1
137 if (start_at + i + 1) % progress_log_interval == 0:
138 progress_percent = (
139 round((start_at + i + 1) / total_issues * 100, 1)
140 if total_issues > 0
141 else 0
142 )
143 logger.info(
144 f"🎫 Progress: {start_at + i + 1}/{total_issues} issues ({progress_percent}%)"
145 )
147 except Exception as e:
148 logger.error(
149 "Failed to parse JIRA issue",
150 issue_id=issue.get("id"),
151 issue_key=issue.get("key"),
152 error=str(e),
153 error_type=type(e).__name__,
154 )
155 # Continue processing other issues instead of failing completely
156 continue
158 # Check if we've processed all issues and store checkpoint
159 start_at += len(issues)
161 if start_at >= total_issues:
162 logger.info(
163 f"✅ Completed JIRA issue retrieval: "
164 f"{start_at} issues attempted, "
165 f"{processed_count} successfully processed"
166 )
167 break