ingestr 0.13.2__py3-none-any.whl → 0.14.104__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.
- ingestr/conftest.py +72 -0
- ingestr/main.py +134 -87
- ingestr/src/adjust/__init__.py +4 -4
- ingestr/src/adjust/adjust_helpers.py +7 -3
- ingestr/src/airtable/__init__.py +3 -2
- ingestr/src/allium/__init__.py +128 -0
- ingestr/src/anthropic/__init__.py +277 -0
- ingestr/src/anthropic/helpers.py +525 -0
- ingestr/src/applovin/__init__.py +262 -0
- ingestr/src/applovin_max/__init__.py +117 -0
- ingestr/src/appsflyer/__init__.py +325 -0
- ingestr/src/appsflyer/client.py +49 -45
- ingestr/src/appstore/__init__.py +1 -0
- ingestr/src/arrow/__init__.py +9 -1
- ingestr/src/asana_source/__init__.py +1 -1
- ingestr/src/attio/__init__.py +102 -0
- ingestr/src/attio/helpers.py +65 -0
- ingestr/src/blob.py +38 -11
- ingestr/src/buildinfo.py +1 -0
- ingestr/src/chess/__init__.py +1 -1
- ingestr/src/clickup/__init__.py +85 -0
- ingestr/src/clickup/helpers.py +47 -0
- ingestr/src/collector/spinner.py +43 -0
- ingestr/src/couchbase_source/__init__.py +118 -0
- ingestr/src/couchbase_source/helpers.py +135 -0
- ingestr/src/cursor/__init__.py +83 -0
- ingestr/src/cursor/helpers.py +188 -0
- ingestr/src/destinations.py +520 -33
- ingestr/src/docebo/__init__.py +589 -0
- ingestr/src/docebo/client.py +435 -0
- ingestr/src/docebo/helpers.py +97 -0
- ingestr/src/elasticsearch/__init__.py +80 -0
- ingestr/src/elasticsearch/helpers.py +138 -0
- ingestr/src/errors.py +8 -0
- ingestr/src/facebook_ads/__init__.py +47 -28
- ingestr/src/facebook_ads/helpers.py +59 -37
- ingestr/src/facebook_ads/settings.py +2 -0
- ingestr/src/facebook_ads/utils.py +39 -0
- ingestr/src/factory.py +116 -2
- ingestr/src/filesystem/__init__.py +8 -3
- ingestr/src/filters.py +46 -3
- ingestr/src/fluxx/__init__.py +9906 -0
- ingestr/src/fluxx/helpers.py +209 -0
- ingestr/src/frankfurter/__init__.py +157 -0
- ingestr/src/frankfurter/helpers.py +48 -0
- ingestr/src/freshdesk/__init__.py +89 -0
- ingestr/src/freshdesk/freshdesk_client.py +137 -0
- ingestr/src/freshdesk/settings.py +9 -0
- ingestr/src/fundraiseup/__init__.py +95 -0
- ingestr/src/fundraiseup/client.py +81 -0
- ingestr/src/github/__init__.py +41 -6
- ingestr/src/github/helpers.py +5 -5
- ingestr/src/google_analytics/__init__.py +22 -4
- ingestr/src/google_analytics/helpers.py +124 -6
- ingestr/src/google_sheets/__init__.py +4 -4
- ingestr/src/google_sheets/helpers/data_processing.py +2 -2
- ingestr/src/hostaway/__init__.py +302 -0
- ingestr/src/hostaway/client.py +288 -0
- ingestr/src/http/__init__.py +35 -0
- ingestr/src/http/readers.py +114 -0
- ingestr/src/http_client.py +24 -0
- ingestr/src/hubspot/__init__.py +66 -23
- ingestr/src/hubspot/helpers.py +52 -22
- ingestr/src/hubspot/settings.py +14 -7
- ingestr/src/influxdb/__init__.py +46 -0
- ingestr/src/influxdb/client.py +34 -0
- ingestr/src/intercom/__init__.py +142 -0
- ingestr/src/intercom/helpers.py +674 -0
- ingestr/src/intercom/settings.py +279 -0
- ingestr/src/isoc_pulse/__init__.py +159 -0
- ingestr/src/jira_source/__init__.py +340 -0
- ingestr/src/jira_source/helpers.py +439 -0
- ingestr/src/jira_source/settings.py +170 -0
- ingestr/src/kafka/__init__.py +4 -1
- ingestr/src/kinesis/__init__.py +139 -0
- ingestr/src/kinesis/helpers.py +82 -0
- ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
- ingestr/src/linear/__init__.py +634 -0
- ingestr/src/linear/helpers.py +111 -0
- ingestr/src/linkedin_ads/helpers.py +0 -1
- ingestr/src/loader.py +69 -0
- ingestr/src/mailchimp/__init__.py +126 -0
- ingestr/src/mailchimp/helpers.py +226 -0
- ingestr/src/mailchimp/settings.py +164 -0
- ingestr/src/masking.py +344 -0
- ingestr/src/mixpanel/__init__.py +62 -0
- ingestr/src/mixpanel/client.py +99 -0
- ingestr/src/monday/__init__.py +246 -0
- ingestr/src/monday/helpers.py +392 -0
- ingestr/src/monday/settings.py +328 -0
- ingestr/src/mongodb/__init__.py +72 -8
- ingestr/src/mongodb/helpers.py +915 -38
- ingestr/src/partition.py +32 -0
- ingestr/src/personio/__init__.py +331 -0
- ingestr/src/personio/helpers.py +86 -0
- ingestr/src/phantombuster/__init__.py +65 -0
- ingestr/src/phantombuster/client.py +87 -0
- ingestr/src/pinterest/__init__.py +82 -0
- ingestr/src/pipedrive/__init__.py +198 -0
- ingestr/src/pipedrive/helpers/__init__.py +23 -0
- ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
- ingestr/src/pipedrive/helpers/pages.py +115 -0
- ingestr/src/pipedrive/settings.py +27 -0
- ingestr/src/pipedrive/typing.py +3 -0
- ingestr/src/plusvibeai/__init__.py +335 -0
- ingestr/src/plusvibeai/helpers.py +544 -0
- ingestr/src/plusvibeai/settings.py +252 -0
- ingestr/src/quickbooks/__init__.py +117 -0
- ingestr/src/resource.py +40 -0
- ingestr/src/revenuecat/__init__.py +83 -0
- ingestr/src/revenuecat/helpers.py +237 -0
- ingestr/src/salesforce/__init__.py +156 -0
- ingestr/src/salesforce/helpers.py +64 -0
- ingestr/src/shopify/__init__.py +1 -17
- ingestr/src/smartsheets/__init__.py +82 -0
- ingestr/src/snapchat_ads/__init__.py +489 -0
- ingestr/src/snapchat_ads/client.py +72 -0
- ingestr/src/snapchat_ads/helpers.py +535 -0
- ingestr/src/socrata_source/__init__.py +83 -0
- ingestr/src/socrata_source/helpers.py +85 -0
- ingestr/src/socrata_source/settings.py +8 -0
- ingestr/src/solidgate/__init__.py +219 -0
- ingestr/src/solidgate/helpers.py +154 -0
- ingestr/src/sources.py +3132 -212
- ingestr/src/stripe_analytics/__init__.py +49 -21
- ingestr/src/stripe_analytics/helpers.py +286 -1
- ingestr/src/stripe_analytics/settings.py +62 -10
- ingestr/src/telemetry/event.py +10 -9
- ingestr/src/tiktok_ads/__init__.py +12 -6
- ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
- ingestr/src/trustpilot/__init__.py +48 -0
- ingestr/src/trustpilot/client.py +48 -0
- ingestr/src/version.py +6 -1
- ingestr/src/wise/__init__.py +68 -0
- ingestr/src/wise/client.py +63 -0
- ingestr/src/zoom/__init__.py +99 -0
- ingestr/src/zoom/helpers.py +102 -0
- ingestr/tests/unit/test_smartsheets.py +133 -0
- ingestr-0.14.104.dist-info/METADATA +563 -0
- ingestr-0.14.104.dist-info/RECORD +203 -0
- ingestr/src/appsflyer/_init_.py +0 -24
- ingestr-0.13.2.dist-info/METADATA +0 -302
- ingestr-0.13.2.dist-info/RECORD +0 -107
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Jira source helpers"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Iterator, Optional
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .settings import API_BASE_PATH, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, REQUEST_TIMEOUT
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JiraAPIError(Exception):
|
|
17
|
+
"""Custom exception for Jira API errors."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
status_code: Optional[int] = None,
|
|
23
|
+
response_text: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.response_text = response_text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JiraAuthenticationError(JiraAPIError):
|
|
31
|
+
"""Exception raised for authentication failures."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JiraRateLimitError(JiraAPIError):
|
|
37
|
+
"""Exception raised when rate limit is exceeded."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JiraClient:
|
|
43
|
+
"""Jira REST API client with authentication and pagination support."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self, base_url: str, email: str, api_token: str, timeout: int = REQUEST_TIMEOUT
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize Jira client with basic auth.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
base_url: Jira instance URL (e.g., https://your-domain.atlassian.net)
|
|
53
|
+
email: User email for authentication
|
|
54
|
+
api_token: API token for authentication
|
|
55
|
+
timeout: Request timeout in seconds
|
|
56
|
+
"""
|
|
57
|
+
self.base_url = base_url.rstrip("/")
|
|
58
|
+
self.api_url = urljoin(self.base_url, API_BASE_PATH)
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
|
|
61
|
+
# Create basic auth header
|
|
62
|
+
credentials = f"{email}:{api_token}"
|
|
63
|
+
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
|
64
|
+
|
|
65
|
+
self.headers = {
|
|
66
|
+
"Authorization": f"Basic {encoded_credentials}",
|
|
67
|
+
"Accept": "application/json",
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def _make_request(
|
|
72
|
+
self,
|
|
73
|
+
endpoint: str,
|
|
74
|
+
params: Optional[Dict[str, Any]] = None,
|
|
75
|
+
method: str = "GET",
|
|
76
|
+
max_retries: int = 3,
|
|
77
|
+
backoff_factor: float = 1.0,
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Make HTTP request to Jira API with retry logic.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
endpoint: API endpoint path
|
|
84
|
+
params: Query parameters
|
|
85
|
+
method: HTTP method
|
|
86
|
+
max_retries: Maximum number of retry attempts
|
|
87
|
+
backoff_factor: Factor for exponential backoff
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
JSON response data
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
JiraAPIError: If request fails after retries
|
|
94
|
+
JiraAuthenticationError: If authentication fails
|
|
95
|
+
JiraRateLimitError: If rate limit is exceeded
|
|
96
|
+
"""
|
|
97
|
+
url = urljoin(self.api_url + "/", endpoint.lstrip("/"))
|
|
98
|
+
|
|
99
|
+
for attempt in range(max_retries + 1):
|
|
100
|
+
try:
|
|
101
|
+
response = requests.request(
|
|
102
|
+
method=method,
|
|
103
|
+
url=url,
|
|
104
|
+
headers=self.headers,
|
|
105
|
+
params=params,
|
|
106
|
+
timeout=self.timeout,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Handle different error status codes
|
|
110
|
+
if response.status_code == 401:
|
|
111
|
+
raise JiraAuthenticationError(
|
|
112
|
+
"Authentication failed. Please check your email and API token.",
|
|
113
|
+
status_code=response.status_code,
|
|
114
|
+
response_text=response.text,
|
|
115
|
+
)
|
|
116
|
+
elif response.status_code == 403:
|
|
117
|
+
raise JiraAuthenticationError(
|
|
118
|
+
"Access forbidden. Please check your permissions.",
|
|
119
|
+
status_code=response.status_code,
|
|
120
|
+
response_text=response.text,
|
|
121
|
+
)
|
|
122
|
+
elif response.status_code == 429:
|
|
123
|
+
# Rate limit exceeded
|
|
124
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
125
|
+
if attempt < max_retries:
|
|
126
|
+
logger.warning(
|
|
127
|
+
f"Rate limit exceeded. Waiting {retry_after} seconds before retry."
|
|
128
|
+
)
|
|
129
|
+
time.sleep(retry_after) # type: ignore
|
|
130
|
+
continue
|
|
131
|
+
else:
|
|
132
|
+
raise JiraRateLimitError(
|
|
133
|
+
f"Rate limit exceeded after {max_retries} retries.",
|
|
134
|
+
status_code=response.status_code,
|
|
135
|
+
response_text=response.text,
|
|
136
|
+
)
|
|
137
|
+
elif response.status_code >= 500:
|
|
138
|
+
# Server error - retry with backoff
|
|
139
|
+
if attempt < max_retries:
|
|
140
|
+
wait_time = backoff_factor * (2**attempt)
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"Server error {response.status_code}. Retrying in {wait_time} seconds."
|
|
143
|
+
)
|
|
144
|
+
time.sleep(wait_time) # type: ignore
|
|
145
|
+
continue
|
|
146
|
+
else:
|
|
147
|
+
raise JiraAPIError(
|
|
148
|
+
f"Server error after {max_retries} retries.",
|
|
149
|
+
status_code=response.status_code,
|
|
150
|
+
response_text=response.text,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Raise for other HTTP errors
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
|
|
156
|
+
# Try to parse JSON response
|
|
157
|
+
try:
|
|
158
|
+
return response.json()
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
raise JiraAPIError(
|
|
161
|
+
f"Invalid JSON response: {str(e)}",
|
|
162
|
+
status_code=response.status_code,
|
|
163
|
+
response_text=response.text,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except requests.RequestException as e:
|
|
167
|
+
if attempt < max_retries:
|
|
168
|
+
wait_time = backoff_factor * (2**attempt)
|
|
169
|
+
logger.warning(
|
|
170
|
+
f"Request failed: {str(e)}. Retrying in {wait_time} seconds."
|
|
171
|
+
)
|
|
172
|
+
time.sleep(wait_time) # type: ignore
|
|
173
|
+
continue
|
|
174
|
+
else:
|
|
175
|
+
raise JiraAPIError(
|
|
176
|
+
f"Request failed after {max_retries} retries: {str(e)}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
raise JiraAPIError(f"Request failed after {max_retries} retries")
|
|
180
|
+
|
|
181
|
+
def get_paginated(
|
|
182
|
+
self,
|
|
183
|
+
endpoint: str,
|
|
184
|
+
params: Optional[Dict[str, Any]] = None,
|
|
185
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
186
|
+
max_results: Optional[int] = None,
|
|
187
|
+
) -> Iterator[Dict[str, Any]]:
|
|
188
|
+
"""
|
|
189
|
+
Get paginated results from Jira API with error handling.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
endpoint: API endpoint path
|
|
193
|
+
params: Query parameters
|
|
194
|
+
page_size: Number of items per page
|
|
195
|
+
max_results: Maximum total results to return
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
Individual items from paginated response
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
JiraAPIError: If pagination fails
|
|
202
|
+
"""
|
|
203
|
+
if params is None:
|
|
204
|
+
params = {}
|
|
205
|
+
|
|
206
|
+
# Validate page size
|
|
207
|
+
page_size = min(max(1, page_size), MAX_PAGE_SIZE)
|
|
208
|
+
params["maxResults"] = page_size
|
|
209
|
+
params["startAt"] = 0
|
|
210
|
+
|
|
211
|
+
total_returned = 0
|
|
212
|
+
consecutive_empty_pages = 0
|
|
213
|
+
max_empty_pages = 3
|
|
214
|
+
|
|
215
|
+
while True:
|
|
216
|
+
try:
|
|
217
|
+
response = self._make_request(endpoint, params)
|
|
218
|
+
|
|
219
|
+
# Handle different response structures
|
|
220
|
+
if "values" in response:
|
|
221
|
+
items = response["values"]
|
|
222
|
+
total = response.get("total", len(items))
|
|
223
|
+
is_last = response.get("isLast", False)
|
|
224
|
+
elif "issues" in response:
|
|
225
|
+
items = response["issues"]
|
|
226
|
+
total = response.get("total", len(items))
|
|
227
|
+
is_last = len(items) < page_size
|
|
228
|
+
elif isinstance(response, list):
|
|
229
|
+
# Some endpoints return arrays directly
|
|
230
|
+
items = response
|
|
231
|
+
total = len(items)
|
|
232
|
+
is_last = True
|
|
233
|
+
else:
|
|
234
|
+
# Single item response
|
|
235
|
+
yield response
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
# Check for empty pages
|
|
239
|
+
if not items:
|
|
240
|
+
consecutive_empty_pages += 1
|
|
241
|
+
if consecutive_empty_pages >= max_empty_pages:
|
|
242
|
+
logger.warning(
|
|
243
|
+
f"Received {consecutive_empty_pages} consecutive empty pages, stopping pagination"
|
|
244
|
+
)
|
|
245
|
+
break
|
|
246
|
+
else:
|
|
247
|
+
consecutive_empty_pages = 0
|
|
248
|
+
|
|
249
|
+
for item in items:
|
|
250
|
+
if max_results and total_returned >= max_results:
|
|
251
|
+
return
|
|
252
|
+
yield item
|
|
253
|
+
total_returned += 1
|
|
254
|
+
|
|
255
|
+
# Check if we've reached the end
|
|
256
|
+
if is_last or len(items) < page_size:
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
# Check if we've got all available items
|
|
260
|
+
if total and total_returned >= total:
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
# Move to next page
|
|
264
|
+
params["startAt"] += page_size
|
|
265
|
+
|
|
266
|
+
# Safety check to prevent infinite loops
|
|
267
|
+
if params["startAt"] > 100000: # Arbitrary large number
|
|
268
|
+
logger.warning(
|
|
269
|
+
f"Pagination safety limit reached for {endpoint}, stopping"
|
|
270
|
+
)
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
except JiraAPIError as e:
|
|
274
|
+
logger.error(f"API error during pagination of {endpoint}: {str(e)}")
|
|
275
|
+
raise
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error(
|
|
278
|
+
f"Unexpected error during pagination of {endpoint}: {str(e)}"
|
|
279
|
+
)
|
|
280
|
+
raise JiraAPIError(f"Pagination failed: {str(e)}")
|
|
281
|
+
|
|
282
|
+
def search_issues(
|
|
283
|
+
self,
|
|
284
|
+
jql: str,
|
|
285
|
+
fields: Optional[str] = None,
|
|
286
|
+
expand: Optional[str] = None,
|
|
287
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
288
|
+
max_results: Optional[int] = None,
|
|
289
|
+
) -> Iterator[Dict[str, Any]]:
|
|
290
|
+
"""
|
|
291
|
+
Search for issues using JQL.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
jql: JQL query string
|
|
295
|
+
fields: Comma-separated list of fields to return
|
|
296
|
+
expand: Comma-separated list of fields to expand
|
|
297
|
+
page_size: Number of items per page
|
|
298
|
+
max_results: Maximum total results to return
|
|
299
|
+
|
|
300
|
+
Yields:
|
|
301
|
+
Issue data
|
|
302
|
+
"""
|
|
303
|
+
params = {"jql": jql}
|
|
304
|
+
if fields:
|
|
305
|
+
params["fields"] = fields
|
|
306
|
+
if expand:
|
|
307
|
+
params["expand"] = expand
|
|
308
|
+
|
|
309
|
+
yield from self.get_paginated(
|
|
310
|
+
"search/jql", params=params, page_size=page_size, max_results=max_results
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def get_projects(
|
|
314
|
+
self, expand: Optional[str] = None, recent: Optional[int] = None
|
|
315
|
+
) -> Iterator[Dict[str, Any]]:
|
|
316
|
+
"""
|
|
317
|
+
Get all projects.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
expand: Comma-separated list of fields to expand
|
|
321
|
+
recent: Number of recent projects to return
|
|
322
|
+
|
|
323
|
+
Yields:
|
|
324
|
+
Project data
|
|
325
|
+
"""
|
|
326
|
+
params = {}
|
|
327
|
+
if expand:
|
|
328
|
+
params["expand"] = expand
|
|
329
|
+
if recent:
|
|
330
|
+
params["recent"] = str(recent)
|
|
331
|
+
|
|
332
|
+
yield from self.get_paginated("project", params=params)
|
|
333
|
+
|
|
334
|
+
def get_users(
|
|
335
|
+
self,
|
|
336
|
+
username: Optional[str] = None,
|
|
337
|
+
account_id: Optional[str] = None,
|
|
338
|
+
start_at: int = 0,
|
|
339
|
+
max_results: int = DEFAULT_PAGE_SIZE,
|
|
340
|
+
) -> Iterator[Dict[str, Any]]:
|
|
341
|
+
"""
|
|
342
|
+
Get users.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
username: Username to search for
|
|
346
|
+
account_id: Account ID to search for
|
|
347
|
+
start_at: Starting index
|
|
348
|
+
max_results: Maximum results per page
|
|
349
|
+
|
|
350
|
+
Yields:
|
|
351
|
+
User data
|
|
352
|
+
"""
|
|
353
|
+
params = {
|
|
354
|
+
"startAt": str(start_at),
|
|
355
|
+
"maxResults": str(min(max_results, MAX_PAGE_SIZE)),
|
|
356
|
+
}
|
|
357
|
+
if username:
|
|
358
|
+
params["username"] = username
|
|
359
|
+
if account_id:
|
|
360
|
+
params["accountId"] = account_id
|
|
361
|
+
|
|
362
|
+
yield from self.get_paginated("users/search", params=params)
|
|
363
|
+
|
|
364
|
+
def get_issue_types(self) -> Iterator[Dict[str, Any]]:
|
|
365
|
+
"""Get all issue types."""
|
|
366
|
+
response = self._make_request("issuetype")
|
|
367
|
+
if isinstance(response, list):
|
|
368
|
+
for issue_type in response:
|
|
369
|
+
yield issue_type
|
|
370
|
+
|
|
371
|
+
def get_statuses(self) -> Iterator[Dict[str, Any]]:
|
|
372
|
+
"""Get all statuses."""
|
|
373
|
+
response = self._make_request("status")
|
|
374
|
+
if isinstance(response, list):
|
|
375
|
+
for status in response:
|
|
376
|
+
yield status
|
|
377
|
+
|
|
378
|
+
def get_priorities(self) -> Iterator[Dict[str, Any]]:
|
|
379
|
+
"""Get all priorities."""
|
|
380
|
+
response = self._make_request("priority")
|
|
381
|
+
if isinstance(response, list):
|
|
382
|
+
for priority in response:
|
|
383
|
+
yield priority
|
|
384
|
+
|
|
385
|
+
def get_resolutions(self) -> Iterator[Dict[str, Any]]:
|
|
386
|
+
"""Get all resolutions."""
|
|
387
|
+
response = self._make_request("resolution")
|
|
388
|
+
if isinstance(response, list):
|
|
389
|
+
for resolution in response:
|
|
390
|
+
yield resolution
|
|
391
|
+
|
|
392
|
+
def get_project_versions(self, project_key: str) -> Iterator[Dict[str, Any]]:
|
|
393
|
+
"""
|
|
394
|
+
Get versions for a specific project.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
project_key: Project key
|
|
398
|
+
|
|
399
|
+
Yields:
|
|
400
|
+
Version data
|
|
401
|
+
"""
|
|
402
|
+
yield from self.get_paginated(f"project/{project_key}/version")
|
|
403
|
+
|
|
404
|
+
def get_project_components(self, project_key: str) -> Iterator[Dict[str, Any]]:
|
|
405
|
+
"""
|
|
406
|
+
Get components for a specific project.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
project_key: Project key
|
|
410
|
+
|
|
411
|
+
Yields:
|
|
412
|
+
Component data
|
|
413
|
+
"""
|
|
414
|
+
yield from self.get_paginated(f"project/{project_key}/component")
|
|
415
|
+
|
|
416
|
+
def get_events(self) -> Iterator[Dict[str, Any]]:
|
|
417
|
+
"""Get all events (issue events like created, updated, etc.)."""
|
|
418
|
+
response = self._make_request("events")
|
|
419
|
+
if isinstance(response, list):
|
|
420
|
+
for event in response:
|
|
421
|
+
yield event
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_client(
|
|
425
|
+
base_url: str, email: str, api_token: str, timeout: int = REQUEST_TIMEOUT
|
|
426
|
+
) -> JiraClient:
|
|
427
|
+
"""
|
|
428
|
+
Create and return a Jira API client.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
base_url: Jira instance URL
|
|
432
|
+
email: User email for authentication
|
|
433
|
+
api_token: API token for authentication
|
|
434
|
+
timeout: Request timeout in seconds
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
JiraClient instance
|
|
438
|
+
"""
|
|
439
|
+
return JiraClient(base_url, email, api_token, timeout)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Jira source settings and constants"""
|
|
2
|
+
|
|
3
|
+
# Default start date for Jira API requests
|
|
4
|
+
DEFAULT_START_DATE = "2010-01-01"
|
|
5
|
+
|
|
6
|
+
# Jira API request timeout in seconds
|
|
7
|
+
REQUEST_TIMEOUT = 300
|
|
8
|
+
|
|
9
|
+
# Default page size for paginated requests
|
|
10
|
+
DEFAULT_PAGE_SIZE = 100
|
|
11
|
+
|
|
12
|
+
# Maximum page size allowed by Jira API
|
|
13
|
+
MAX_PAGE_SIZE = 1000
|
|
14
|
+
|
|
15
|
+
# Base API path for Jira Cloud
|
|
16
|
+
API_BASE_PATH = "/rest/api/3"
|
|
17
|
+
|
|
18
|
+
# Project fields to retrieve from Jira API
|
|
19
|
+
PROJECT_FIELDS = (
|
|
20
|
+
"id",
|
|
21
|
+
"key",
|
|
22
|
+
"name",
|
|
23
|
+
"description",
|
|
24
|
+
"lead",
|
|
25
|
+
"projectCategory",
|
|
26
|
+
"projectTypeKey",
|
|
27
|
+
"simplified",
|
|
28
|
+
"style",
|
|
29
|
+
"favourite",
|
|
30
|
+
"isPrivate",
|
|
31
|
+
"properties",
|
|
32
|
+
"entityId",
|
|
33
|
+
"uuid",
|
|
34
|
+
"insight",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Issue fields to retrieve from Jira API
|
|
38
|
+
ISSUE_FIELDS = (
|
|
39
|
+
"id",
|
|
40
|
+
"key",
|
|
41
|
+
"summary",
|
|
42
|
+
"description",
|
|
43
|
+
"issuetype",
|
|
44
|
+
"status",
|
|
45
|
+
"priority",
|
|
46
|
+
"resolution",
|
|
47
|
+
"assignee",
|
|
48
|
+
"reporter",
|
|
49
|
+
"creator",
|
|
50
|
+
"created",
|
|
51
|
+
"updated",
|
|
52
|
+
"resolutiondate",
|
|
53
|
+
"duedate",
|
|
54
|
+
"components",
|
|
55
|
+
"fixVersions",
|
|
56
|
+
"versions",
|
|
57
|
+
"labels",
|
|
58
|
+
"environment",
|
|
59
|
+
"project",
|
|
60
|
+
"parent",
|
|
61
|
+
"subtasks",
|
|
62
|
+
"issuelinks",
|
|
63
|
+
"votes",
|
|
64
|
+
"watches",
|
|
65
|
+
"worklog",
|
|
66
|
+
"attachments",
|
|
67
|
+
"comment",
|
|
68
|
+
"customfield_*",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# User fields to retrieve from Jira API
|
|
72
|
+
USER_FIELDS = (
|
|
73
|
+
"accountId",
|
|
74
|
+
"accountType",
|
|
75
|
+
"emailAddress",
|
|
76
|
+
"displayName",
|
|
77
|
+
"active",
|
|
78
|
+
"timeZone",
|
|
79
|
+
"groups",
|
|
80
|
+
"applicationRoles",
|
|
81
|
+
"expand",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Board fields to retrieve from Jira API (for Agile/Scrum boards)
|
|
85
|
+
BOARD_FIELDS = (
|
|
86
|
+
"id",
|
|
87
|
+
"name",
|
|
88
|
+
"type",
|
|
89
|
+
"location",
|
|
90
|
+
"filter",
|
|
91
|
+
"subQuery",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Sprint fields to retrieve from Jira API
|
|
95
|
+
SPRINT_FIELDS = (
|
|
96
|
+
"id",
|
|
97
|
+
"name",
|
|
98
|
+
"state",
|
|
99
|
+
"startDate",
|
|
100
|
+
"endDate",
|
|
101
|
+
"completeDate",
|
|
102
|
+
"originBoardId",
|
|
103
|
+
"goal",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Issue type fields to retrieve from Jira API
|
|
107
|
+
ISSUE_TYPE_FIELDS = (
|
|
108
|
+
"id",
|
|
109
|
+
"name",
|
|
110
|
+
"description",
|
|
111
|
+
"iconUrl",
|
|
112
|
+
"subtask",
|
|
113
|
+
"avatarId",
|
|
114
|
+
"hierarchyLevel",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Status fields to retrieve from Jira API
|
|
118
|
+
STATUS_FIELDS = (
|
|
119
|
+
"id",
|
|
120
|
+
"name",
|
|
121
|
+
"description",
|
|
122
|
+
"iconUrl",
|
|
123
|
+
"statusCategory",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Priority fields to retrieve from Jira API
|
|
127
|
+
PRIORITY_FIELDS = (
|
|
128
|
+
"id",
|
|
129
|
+
"name",
|
|
130
|
+
"description",
|
|
131
|
+
"iconUrl",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Resolution fields to retrieve from Jira API
|
|
135
|
+
RESOLUTION_FIELDS = (
|
|
136
|
+
"id",
|
|
137
|
+
"name",
|
|
138
|
+
"description",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Version fields to retrieve from Jira API
|
|
142
|
+
VERSION_FIELDS = (
|
|
143
|
+
"id",
|
|
144
|
+
"name",
|
|
145
|
+
"description",
|
|
146
|
+
"archived",
|
|
147
|
+
"released",
|
|
148
|
+
"startDate",
|
|
149
|
+
"releaseDate",
|
|
150
|
+
"overdue",
|
|
151
|
+
"userStartDate",
|
|
152
|
+
"userReleaseDate",
|
|
153
|
+
"project",
|
|
154
|
+
"projectId",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Component fields to retrieve from Jira API
|
|
158
|
+
COMPONENT_FIELDS = (
|
|
159
|
+
"id",
|
|
160
|
+
"name",
|
|
161
|
+
"description",
|
|
162
|
+
"lead",
|
|
163
|
+
"assigneeType",
|
|
164
|
+
"assignee",
|
|
165
|
+
"realAssigneeType",
|
|
166
|
+
"realAssignee",
|
|
167
|
+
"isAssigneeTypeValid",
|
|
168
|
+
"project",
|
|
169
|
+
"projectId",
|
|
170
|
+
)
|
ingestr/src/kafka/__init__.py
CHANGED
|
@@ -83,7 +83,7 @@ def kafka_consumer(
|
|
|
83
83
|
# read messages up to the maximum offsets,
|
|
84
84
|
# not waiting for new messages
|
|
85
85
|
with closing(consumer):
|
|
86
|
-
while
|
|
86
|
+
while True:
|
|
87
87
|
messages = consumer.consume(batch_size, timeout=batch_timeout)
|
|
88
88
|
if not messages:
|
|
89
89
|
break
|
|
@@ -101,3 +101,6 @@ def kafka_consumer(
|
|
|
101
101
|
tracker.renew(msg)
|
|
102
102
|
|
|
103
103
|
yield batch
|
|
104
|
+
|
|
105
|
+
if tracker.has_unread is False:
|
|
106
|
+
return
|