omniload 0.0.0.dev0__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.
Files changed (218) hide show
  1. omniload/conftest.py +72 -0
  2. omniload/main.py +810 -0
  3. omniload/src/.gitignore +10 -0
  4. omniload/src/adjust/__init__.py +108 -0
  5. omniload/src/adjust/adjust_helpers.py +122 -0
  6. omniload/src/airtable/__init__.py +84 -0
  7. omniload/src/allium/__init__.py +128 -0
  8. omniload/src/anthropic/__init__.py +277 -0
  9. omniload/src/anthropic/helpers.py +525 -0
  10. omniload/src/applovin/__init__.py +316 -0
  11. omniload/src/applovin_max/__init__.py +117 -0
  12. omniload/src/appsflyer/__init__.py +325 -0
  13. omniload/src/appsflyer/client.py +110 -0
  14. omniload/src/appstore/__init__.py +142 -0
  15. omniload/src/appstore/client.py +126 -0
  16. omniload/src/appstore/errors.py +15 -0
  17. omniload/src/appstore/models.py +117 -0
  18. omniload/src/appstore/resources.py +179 -0
  19. omniload/src/arrow/__init__.py +81 -0
  20. omniload/src/asana_source/__init__.py +281 -0
  21. omniload/src/asana_source/helpers.py +30 -0
  22. omniload/src/asana_source/settings.py +158 -0
  23. omniload/src/attio/__init__.py +102 -0
  24. omniload/src/attio/helpers.py +65 -0
  25. omniload/src/blob.py +95 -0
  26. omniload/src/bruin/__init__.py +76 -0
  27. omniload/src/chess/__init__.py +180 -0
  28. omniload/src/chess/helpers.py +35 -0
  29. omniload/src/chess/settings.py +18 -0
  30. omniload/src/clickup/__init__.py +85 -0
  31. omniload/src/clickup/helpers.py +47 -0
  32. omniload/src/collector/spinner.py +43 -0
  33. omniload/src/couchbase_source/__init__.py +118 -0
  34. omniload/src/couchbase_source/helpers.py +135 -0
  35. omniload/src/cursor/__init__.py +83 -0
  36. omniload/src/cursor/helpers.py +188 -0
  37. omniload/src/customer_io/__init__.py +486 -0
  38. omniload/src/customer_io/helpers.py +530 -0
  39. omniload/src/destinations.py +982 -0
  40. omniload/src/docebo/__init__.py +589 -0
  41. omniload/src/docebo/client.py +435 -0
  42. omniload/src/docebo/helpers.py +97 -0
  43. omniload/src/dune/__init__.py +104 -0
  44. omniload/src/dune/helpers.py +108 -0
  45. omniload/src/dynamodb/__init__.py +86 -0
  46. omniload/src/elasticsearch/__init__.py +80 -0
  47. omniload/src/elasticsearch/helpers.py +141 -0
  48. omniload/src/errors.py +26 -0
  49. omniload/src/facebook_ads/__init__.py +403 -0
  50. omniload/src/facebook_ads/exceptions.py +19 -0
  51. omniload/src/facebook_ads/helpers.py +296 -0
  52. omniload/src/facebook_ads/settings.py +224 -0
  53. omniload/src/facebook_ads/utils.py +53 -0
  54. omniload/src/factory.py +305 -0
  55. omniload/src/filesystem/__init__.py +133 -0
  56. omniload/src/filesystem/helpers.py +114 -0
  57. omniload/src/filesystem/readers.py +187 -0
  58. omniload/src/filters.py +62 -0
  59. omniload/src/fireflies/__init__.py +151 -0
  60. omniload/src/fireflies/helpers.py +753 -0
  61. omniload/src/fluxx/__init__.py +10013 -0
  62. omniload/src/fluxx/helpers.py +233 -0
  63. omniload/src/frankfurter/__init__.py +157 -0
  64. omniload/src/frankfurter/helpers.py +48 -0
  65. omniload/src/freshdesk/__init__.py +103 -0
  66. omniload/src/freshdesk/freshdesk_client.py +151 -0
  67. omniload/src/freshdesk/settings.py +23 -0
  68. omniload/src/fundraiseup/__init__.py +95 -0
  69. omniload/src/fundraiseup/client.py +81 -0
  70. omniload/src/github/__init__.py +202 -0
  71. omniload/src/github/helpers.py +207 -0
  72. omniload/src/github/queries.py +129 -0
  73. omniload/src/github/settings.py +24 -0
  74. omniload/src/google_ads/__init__.py +198 -0
  75. omniload/src/google_ads/field.py +17 -0
  76. omniload/src/google_ads/metrics.py +254 -0
  77. omniload/src/google_ads/predicates.py +37 -0
  78. omniload/src/google_ads/reports.py +411 -0
  79. omniload/src/google_ads/test_google_ads.py +184 -0
  80. omniload/src/google_analytics/__init__.py +144 -0
  81. omniload/src/google_analytics/helpers.py +312 -0
  82. omniload/src/google_sheets/README.md +95 -0
  83. omniload/src/google_sheets/__init__.py +166 -0
  84. omniload/src/google_sheets/helpers/__init__.py +15 -0
  85. omniload/src/google_sheets/helpers/api_calls.py +160 -0
  86. omniload/src/google_sheets/helpers/data_processing.py +316 -0
  87. omniload/src/gorgias/__init__.py +595 -0
  88. omniload/src/gorgias/helpers.py +166 -0
  89. omniload/src/hostaway/__init__.py +302 -0
  90. omniload/src/hostaway/client.py +288 -0
  91. omniload/src/http/__init__.py +38 -0
  92. omniload/src/http/readers.py +146 -0
  93. omniload/src/http_client.py +24 -0
  94. omniload/src/hubspot/__init__.py +800 -0
  95. omniload/src/hubspot/helpers.py +417 -0
  96. omniload/src/hubspot/settings.py +329 -0
  97. omniload/src/indeed/__init__.py +153 -0
  98. omniload/src/indeed/helpers.py +228 -0
  99. omniload/src/influxdb/__init__.py +46 -0
  100. omniload/src/influxdb/client.py +34 -0
  101. omniload/src/intercom/__init__.py +142 -0
  102. omniload/src/intercom/helpers.py +674 -0
  103. omniload/src/intercom/settings.py +279 -0
  104. omniload/src/isoc_pulse/__init__.py +159 -0
  105. omniload/src/jira_source/__init__.py +377 -0
  106. omniload/src/jira_source/helpers.py +510 -0
  107. omniload/src/jira_source/settings.py +184 -0
  108. omniload/src/kafka/__init__.py +120 -0
  109. omniload/src/kafka/helpers.py +241 -0
  110. omniload/src/kinesis/__init__.py +153 -0
  111. omniload/src/kinesis/helpers.py +96 -0
  112. omniload/src/klaviyo/__init__.py +237 -0
  113. omniload/src/klaviyo/client.py +212 -0
  114. omniload/src/klaviyo/helpers.py +19 -0
  115. omniload/src/linear/__init__.py +634 -0
  116. omniload/src/linear/helpers.py +111 -0
  117. omniload/src/linkedin_ads/__init__.py +266 -0
  118. omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
  119. omniload/src/linkedin_ads/helpers.py +246 -0
  120. omniload/src/loader.py +69 -0
  121. omniload/src/mailchimp/__init__.py +126 -0
  122. omniload/src/mailchimp/helpers.py +226 -0
  123. omniload/src/mailchimp/settings.py +164 -0
  124. omniload/src/masking.py +344 -0
  125. omniload/src/mixpanel/__init__.py +62 -0
  126. omniload/src/mixpanel/client.py +104 -0
  127. omniload/src/monday/__init__.py +246 -0
  128. omniload/src/monday/helpers.py +392 -0
  129. omniload/src/monday/settings.py +325 -0
  130. omniload/src/mongodb/__init__.py +281 -0
  131. omniload/src/mongodb/helpers.py +975 -0
  132. omniload/src/notion/__init__.py +69 -0
  133. omniload/src/notion/helpers/__init__.py +14 -0
  134. omniload/src/notion/helpers/client.py +178 -0
  135. omniload/src/notion/helpers/database.py +92 -0
  136. omniload/src/notion/settings.py +17 -0
  137. omniload/src/partition.py +32 -0
  138. omniload/src/personio/__init__.py +345 -0
  139. omniload/src/personio/helpers.py +100 -0
  140. omniload/src/phantombuster/__init__.py +65 -0
  141. omniload/src/phantombuster/client.py +87 -0
  142. omniload/src/pinterest/__init__.py +82 -0
  143. omniload/src/pipedrive/__init__.py +212 -0
  144. omniload/src/pipedrive/helpers/__init__.py +37 -0
  145. omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
  146. omniload/src/pipedrive/helpers/pages.py +129 -0
  147. omniload/src/pipedrive/settings.py +41 -0
  148. omniload/src/pipedrive/typing.py +17 -0
  149. omniload/src/plusvibeai/__init__.py +335 -0
  150. omniload/src/plusvibeai/helpers.py +544 -0
  151. omniload/src/plusvibeai/settings.py +252 -0
  152. omniload/src/primer/__init__.py +45 -0
  153. omniload/src/primer/helpers.py +79 -0
  154. omniload/src/quickbooks/__init__.py +117 -0
  155. omniload/src/reddit_ads/__init__.py +183 -0
  156. omniload/src/reddit_ads/helpers.py +232 -0
  157. omniload/src/resource.py +40 -0
  158. omniload/src/revenuecat/__init__.py +83 -0
  159. omniload/src/revenuecat/helpers.py +237 -0
  160. omniload/src/salesforce/__init__.py +170 -0
  161. omniload/src/salesforce/helpers.py +78 -0
  162. omniload/src/shopify/__init__.py +1953 -0
  163. omniload/src/shopify/exceptions.py +17 -0
  164. omniload/src/shopify/helpers.py +202 -0
  165. omniload/src/shopify/settings.py +19 -0
  166. omniload/src/slack/__init__.py +290 -0
  167. omniload/src/slack/helpers.py +218 -0
  168. omniload/src/slack/settings.py +36 -0
  169. omniload/src/smartsheets/__init__.py +82 -0
  170. omniload/src/snapchat_ads/__init__.py +455 -0
  171. omniload/src/snapchat_ads/client.py +72 -0
  172. omniload/src/snapchat_ads/helpers.py +630 -0
  173. omniload/src/snapchat_ads/settings.py +130 -0
  174. omniload/src/socrata_source/__init__.py +83 -0
  175. omniload/src/socrata_source/helpers.py +85 -0
  176. omniload/src/socrata_source/settings.py +8 -0
  177. omniload/src/solidgate/__init__.py +219 -0
  178. omniload/src/solidgate/helpers.py +154 -0
  179. omniload/src/sources.py +5408 -0
  180. omniload/src/sql_database/__init__.py +0 -0
  181. omniload/src/sql_database/callbacks.py +66 -0
  182. omniload/src/stripe_analytics/__init__.py +183 -0
  183. omniload/src/stripe_analytics/helpers.py +386 -0
  184. omniload/src/stripe_analytics/settings.py +80 -0
  185. omniload/src/table_definition.py +15 -0
  186. omniload/src/testdata/fakebqcredentials.json +14 -0
  187. omniload/src/tiktok_ads/__init__.py +150 -0
  188. omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
  189. omniload/src/time.py +11 -0
  190. omniload/src/trustpilot/__init__.py +48 -0
  191. omniload/src/trustpilot/client.py +48 -0
  192. omniload/src/version.py +6 -0
  193. omniload/src/wise/__init__.py +68 -0
  194. omniload/src/wise/client.py +63 -0
  195. omniload/src/zendesk/__init__.py +480 -0
  196. omniload/src/zendesk/helpers/__init__.py +39 -0
  197. omniload/src/zendesk/helpers/api_helpers.py +119 -0
  198. omniload/src/zendesk/helpers/credentials.py +68 -0
  199. omniload/src/zendesk/helpers/talk_api.py +132 -0
  200. omniload/src/zendesk/settings.py +71 -0
  201. omniload/src/zoom/__init__.py +99 -0
  202. omniload/src/zoom/helpers.py +102 -0
  203. omniload/testdata/.gitignore +2 -0
  204. omniload/testdata/create_replace.csv +21 -0
  205. omniload/testdata/delete_insert_expected.csv +6 -0
  206. omniload/testdata/delete_insert_part1.csv +5 -0
  207. omniload/testdata/delete_insert_part2.csv +6 -0
  208. omniload/testdata/merge_expected.csv +5 -0
  209. omniload/testdata/merge_part1.csv +4 -0
  210. omniload/testdata/merge_part2.csv +5 -0
  211. omniload/tests/unit/test_smartsheets.py +133 -0
  212. omniload-0.0.0.dev0.dist-info/METADATA +439 -0
  213. omniload-0.0.0.dev0.dist-info/RECORD +218 -0
  214. omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
  215. omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  216. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
  217. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  218. omniload-0.0.0.dev0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,510 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Jira source helpers"""
16
+
17
+ import base64
18
+ import logging
19
+ import time
20
+ from typing import Any, Dict, Iterator, Optional
21
+ from urllib.parse import urljoin
22
+
23
+ import requests
24
+
25
+ from .settings import API_BASE_PATH, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, REQUEST_TIMEOUT
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class JiraAPIError(Exception):
31
+ """Custom exception for Jira API errors."""
32
+
33
+ def __init__(
34
+ self,
35
+ message: str,
36
+ status_code: Optional[int] = None,
37
+ response_text: Optional[str] = None,
38
+ ):
39
+ super().__init__(message)
40
+ self.status_code = status_code
41
+ self.response_text = response_text
42
+
43
+
44
+ class JiraAuthenticationError(JiraAPIError):
45
+ """Exception raised for authentication failures."""
46
+
47
+ pass
48
+
49
+
50
+ class JiraRateLimitError(JiraAPIError):
51
+ """Exception raised when rate limit is exceeded."""
52
+
53
+ pass
54
+
55
+
56
+ class JiraClient:
57
+ """Jira REST API client with authentication and pagination support."""
58
+
59
+ def __init__(
60
+ self, base_url: str, email: str, api_token: str, timeout: int = REQUEST_TIMEOUT
61
+ ):
62
+ """
63
+ Initialize Jira client with basic auth.
64
+
65
+ Args:
66
+ base_url: Jira instance URL (e.g., https://your-domain.atlassian.net)
67
+ email: User email for authentication
68
+ api_token: API token for authentication
69
+ timeout: Request timeout in seconds
70
+ """
71
+ self.base_url = base_url.rstrip("/")
72
+ self.api_url = urljoin(self.base_url, API_BASE_PATH)
73
+ self.timeout = timeout
74
+
75
+ # Create basic auth header
76
+ credentials = f"{email}:{api_token}"
77
+ encoded_credentials = base64.b64encode(credentials.encode()).decode()
78
+
79
+ self.headers = {
80
+ "Authorization": f"Basic {encoded_credentials}",
81
+ "Accept": "application/json",
82
+ "Content-Type": "application/json",
83
+ }
84
+
85
+ def _make_request(
86
+ self,
87
+ endpoint: str,
88
+ params: Optional[Dict[str, Any]] = None,
89
+ method: str = "GET",
90
+ max_retries: int = 3,
91
+ backoff_factor: float = 1.0,
92
+ ) -> Dict[str, Any]:
93
+ """
94
+ Make HTTP request to Jira API with retry logic.
95
+
96
+ Args:
97
+ endpoint: API endpoint path
98
+ params: Query parameters
99
+ method: HTTP method
100
+ max_retries: Maximum number of retry attempts
101
+ backoff_factor: Factor for exponential backoff
102
+
103
+ Returns:
104
+ JSON response data
105
+
106
+ Raises:
107
+ JiraAPIError: If request fails after retries
108
+ JiraAuthenticationError: If authentication fails
109
+ JiraRateLimitError: If rate limit is exceeded
110
+ """
111
+ url = urljoin(self.api_url + "/", endpoint.lstrip("/"))
112
+
113
+ for attempt in range(max_retries + 1):
114
+ try:
115
+ response = requests.request(
116
+ method=method,
117
+ url=url,
118
+ headers=self.headers,
119
+ params=params,
120
+ timeout=self.timeout,
121
+ )
122
+
123
+ # Handle different error status codes
124
+ if response.status_code == 401:
125
+ raise JiraAuthenticationError(
126
+ "Authentication failed. Please check your email and API token.",
127
+ status_code=response.status_code,
128
+ response_text=response.text,
129
+ )
130
+ elif response.status_code == 403:
131
+ raise JiraAuthenticationError(
132
+ "Access forbidden. Please check your permissions.",
133
+ status_code=response.status_code,
134
+ response_text=response.text,
135
+ )
136
+ elif response.status_code == 429:
137
+ # Rate limit exceeded
138
+ retry_after = int(response.headers.get("Retry-After", 60))
139
+ if attempt < max_retries:
140
+ logger.warning(
141
+ f"Rate limit exceeded. Waiting {retry_after} seconds before retry."
142
+ )
143
+ time.sleep(retry_after) # type: ignore
144
+ continue
145
+ else:
146
+ raise JiraRateLimitError(
147
+ f"Rate limit exceeded after {max_retries} retries.",
148
+ status_code=response.status_code,
149
+ response_text=response.text,
150
+ )
151
+ elif response.status_code >= 500:
152
+ # Server error - retry with backoff
153
+ if attempt < max_retries:
154
+ wait_time = backoff_factor * (2**attempt)
155
+ logger.warning(
156
+ f"Server error {response.status_code}. Retrying in {wait_time} seconds."
157
+ )
158
+ time.sleep(wait_time) # type: ignore
159
+ continue
160
+ else:
161
+ raise JiraAPIError(
162
+ f"Server error after {max_retries} retries.",
163
+ status_code=response.status_code,
164
+ response_text=response.text,
165
+ )
166
+
167
+ # Raise for other HTTP errors
168
+ response.raise_for_status()
169
+
170
+ # Try to parse JSON response
171
+ try:
172
+ return response.json()
173
+ except ValueError as e:
174
+ raise JiraAPIError(
175
+ f"Invalid JSON response: {str(e)}",
176
+ status_code=response.status_code,
177
+ response_text=response.text,
178
+ )
179
+
180
+ except requests.RequestException as e:
181
+ if attempt < max_retries:
182
+ wait_time = backoff_factor * (2**attempt)
183
+ logger.warning(
184
+ f"Request failed: {str(e)}. Retrying in {wait_time} seconds."
185
+ )
186
+ time.sleep(wait_time) # type: ignore
187
+ continue
188
+ else:
189
+ raise JiraAPIError(
190
+ f"Request failed after {max_retries} retries: {str(e)}"
191
+ )
192
+
193
+ raise JiraAPIError(f"Request failed after {max_retries} retries")
194
+
195
+ def get_paginated(
196
+ self,
197
+ endpoint: str,
198
+ params: Optional[Dict[str, Any]] = None,
199
+ page_size: int = DEFAULT_PAGE_SIZE,
200
+ max_results: Optional[int] = None,
201
+ ) -> Iterator[Dict[str, Any]]:
202
+ """
203
+ Get paginated results from Jira API with error handling.
204
+
205
+ Args:
206
+ endpoint: API endpoint path
207
+ params: Query parameters
208
+ page_size: Number of items per page
209
+ max_results: Maximum total results to return
210
+
211
+ Yields:
212
+ Individual items from paginated response
213
+
214
+ Raises:
215
+ JiraAPIError: If pagination fails
216
+ """
217
+ if params is None:
218
+ params = {}
219
+
220
+ # Validate page size
221
+ page_size = min(max(1, page_size), MAX_PAGE_SIZE)
222
+ params["maxResults"] = page_size
223
+ params["startAt"] = 0
224
+
225
+ total_returned = 0
226
+ consecutive_empty_pages = 0
227
+ max_empty_pages = 3
228
+ next_page_token = None
229
+
230
+ while True:
231
+ try:
232
+ if next_page_token:
233
+ params["nextPageToken"] = next_page_token
234
+ elif "nextPageToken" in params:
235
+ del params["nextPageToken"]
236
+ response = self._make_request(endpoint, params)
237
+
238
+ # Handle different response structures
239
+ if "values" in response:
240
+ items = response["values"]
241
+ total = response.get("total", len(items))
242
+ is_last = response.get("isLast", False)
243
+ next_page_token = response.get("nextPageToken")
244
+ elif "issues" in response:
245
+ items = response["issues"]
246
+ total = response.get("total", len(items))
247
+ is_last = response.get("isLast", len(items) < page_size)
248
+ next_page_token = response.get("nextPageToken")
249
+ elif isinstance(response, list):
250
+ # Some endpoints return arrays directly
251
+ items = response
252
+ total = len(items)
253
+ is_last = True
254
+ else:
255
+ # Single item response
256
+ yield response
257
+ break
258
+
259
+ # Check for empty pages
260
+ if not items:
261
+ consecutive_empty_pages += 1
262
+ if consecutive_empty_pages >= max_empty_pages:
263
+ logger.warning(
264
+ f"Received {consecutive_empty_pages} consecutive empty pages, stopping pagination"
265
+ )
266
+ break
267
+ else:
268
+ consecutive_empty_pages = 0
269
+
270
+ for item in items:
271
+ if max_results and total_returned >= max_results:
272
+ return
273
+ yield item
274
+ total_returned += 1
275
+
276
+ # Check if we've reached the end
277
+ if is_last:
278
+ break
279
+
280
+ # Use nextPageToken if available, otherwise fall back to startAt
281
+ if next_page_token:
282
+ continue
283
+ elif len(items) < page_size:
284
+ break
285
+ elif total and total_returned >= total:
286
+ break
287
+ else:
288
+ params["startAt"] += page_size
289
+
290
+ # Safety check to prevent infinite loops
291
+ if params["startAt"] > 100000: # Arbitrary large number
292
+ logger.warning(
293
+ f"Pagination safety limit reached for {endpoint}, stopping"
294
+ )
295
+ break
296
+
297
+ except JiraAPIError as e:
298
+ logger.error(f"API error during pagination of {endpoint}: {str(e)}")
299
+ raise
300
+ except Exception as e:
301
+ logger.error(
302
+ f"Unexpected error during pagination of {endpoint}: {str(e)}"
303
+ )
304
+ raise JiraAPIError(f"Pagination failed: {str(e)}")
305
+
306
+ def search_issues(
307
+ self,
308
+ jql: str,
309
+ fields: Optional[str] = None,
310
+ expand: Optional[str] = None,
311
+ page_size: int = DEFAULT_PAGE_SIZE,
312
+ max_results: Optional[int] = None,
313
+ ) -> Iterator[Dict[str, Any]]:
314
+ """
315
+ Search for issues using JQL.
316
+
317
+ Args:
318
+ jql: JQL query string
319
+ fields: Comma-separated list of fields to return
320
+ expand: Comma-separated list of fields to expand
321
+ page_size: Number of items per page
322
+ max_results: Maximum total results to return
323
+
324
+ Yields:
325
+ Issue data
326
+ """
327
+ params = {"jql": jql}
328
+ if fields:
329
+ params["fields"] = fields
330
+ if expand:
331
+ params["expand"] = expand
332
+
333
+ yield from self.get_paginated(
334
+ "search/jql", params=params, page_size=page_size, max_results=max_results
335
+ )
336
+
337
+ def get_projects(
338
+ self, expand: Optional[str] = None, recent: Optional[int] = None
339
+ ) -> Iterator[Dict[str, Any]]:
340
+ """
341
+ Get all projects.
342
+
343
+ Args:
344
+ expand: Comma-separated list of fields to expand
345
+ recent: Number of recent projects to return
346
+
347
+ Yields:
348
+ Project data
349
+ """
350
+ params = {}
351
+ if expand:
352
+ params["expand"] = expand
353
+ if recent:
354
+ params["recent"] = str(recent)
355
+
356
+ yield from self.get_paginated("project", params=params)
357
+
358
+ def get_users(
359
+ self,
360
+ username: Optional[str] = None,
361
+ account_id: Optional[str] = None,
362
+ start_at: int = 0,
363
+ max_results: int = DEFAULT_PAGE_SIZE,
364
+ ) -> Iterator[Dict[str, Any]]:
365
+ """
366
+ Get users.
367
+
368
+ Args:
369
+ username: Username to search for
370
+ account_id: Account ID to search for
371
+ start_at: Starting index
372
+ max_results: Maximum results per page
373
+
374
+ Yields:
375
+ User data
376
+ """
377
+ params = {
378
+ "startAt": str(start_at),
379
+ "maxResults": str(min(max_results, MAX_PAGE_SIZE)),
380
+ }
381
+ if username:
382
+ params["username"] = username
383
+ if account_id:
384
+ params["accountId"] = account_id
385
+
386
+ yield from self.get_paginated("users/search", params=params)
387
+
388
+ def get_issue_types(self) -> Iterator[Dict[str, Any]]:
389
+ """Get all issue types."""
390
+ response = self._make_request("issuetype")
391
+ if isinstance(response, list):
392
+ for issue_type in response:
393
+ yield issue_type
394
+
395
+ def get_statuses(self) -> Iterator[Dict[str, Any]]:
396
+ """Get all statuses."""
397
+ response = self._make_request("status")
398
+ if isinstance(response, list):
399
+ for status in response:
400
+ yield status
401
+
402
+ def get_priorities(self) -> Iterator[Dict[str, Any]]:
403
+ """Get all priorities."""
404
+ response = self._make_request("priority")
405
+ if isinstance(response, list):
406
+ for priority in response:
407
+ yield priority
408
+
409
+ def get_resolutions(self) -> Iterator[Dict[str, Any]]:
410
+ """Get all resolutions."""
411
+ response = self._make_request("resolution")
412
+ if isinstance(response, list):
413
+ for resolution in response:
414
+ yield resolution
415
+
416
+ def get_project_versions(self, project_key: str) -> Iterator[Dict[str, Any]]:
417
+ """
418
+ Get versions for a specific project.
419
+
420
+ Args:
421
+ project_key: Project key
422
+
423
+ Yields:
424
+ Version data
425
+ """
426
+ yield from self.get_paginated(f"project/{project_key}/version")
427
+
428
+ def get_project_components(self, project_key: str) -> Iterator[Dict[str, Any]]:
429
+ """
430
+ Get components for a specific project.
431
+
432
+ Args:
433
+ project_key: Project key
434
+
435
+ Yields:
436
+ Component data
437
+ """
438
+ yield from self.get_paginated(f"project/{project_key}/component")
439
+
440
+ def get_events(self) -> Iterator[Dict[str, Any]]:
441
+ """Get all events (issue events like created, updated, etc.)."""
442
+ response = self._make_request("events")
443
+ if isinstance(response, list):
444
+ for event in response:
445
+ yield event
446
+
447
+ def get_changelogs_bulk(
448
+ self,
449
+ issue_ids_or_keys: list[str],
450
+ batch_size: int = 1000,
451
+ ) -> Iterator[Dict[str, Any]]:
452
+ url = urljoin(self.api_url + "/", "changelog/bulkfetch")
453
+
454
+ for i in range(0, len(issue_ids_or_keys), batch_size):
455
+ batch = issue_ids_or_keys[i : i + batch_size]
456
+ next_page_token = None
457
+
458
+ while True:
459
+ body: Dict[str, Any] = {
460
+ "issueIdsOrKeys": batch,
461
+ }
462
+
463
+ if next_page_token:
464
+ body["nextPageToken"] = next_page_token
465
+
466
+ response = requests.post(
467
+ url,
468
+ headers=self.headers,
469
+ json=body,
470
+ timeout=self.timeout,
471
+ )
472
+ response.raise_for_status()
473
+ data = response.json()
474
+
475
+ for issue_changelog in data.get("issueChangeLogs", []):
476
+ issue_id = issue_changelog.get("issueId")
477
+ for history in issue_changelog.get("changeHistories", []):
478
+ history["issue_id"] = issue_id
479
+ if history.get("created"):
480
+ from datetime import datetime
481
+
482
+ history["created"] = datetime.fromtimestamp(
483
+ history["created"] / 1000
484
+ ).isoformat()
485
+ for item in history.get("items", []):
486
+ history["from_string"] = item.pop("fromString", None)
487
+ history["to_string"] = item.pop("toString", None)
488
+ yield history
489
+
490
+ next_page_token = data.get("nextPageToken")
491
+ if not next_page_token:
492
+ break
493
+
494
+
495
+ def get_client(
496
+ base_url: str, email: str, api_token: str, timeout: int = REQUEST_TIMEOUT
497
+ ) -> JiraClient:
498
+ """
499
+ Create and return a Jira API client.
500
+
501
+ Args:
502
+ base_url: Jira instance URL
503
+ email: User email for authentication
504
+ api_token: API token for authentication
505
+ timeout: Request timeout in seconds
506
+
507
+ Returns:
508
+ JiraClient instance
509
+ """
510
+ return JiraClient(base_url, email, api_token, timeout)
@@ -0,0 +1,184 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Jira source settings and constants"""
16
+
17
+ # Default start date for Jira API requests
18
+ DEFAULT_START_DATE = "2010-01-01"
19
+
20
+ # Jira API request timeout in seconds
21
+ REQUEST_TIMEOUT = 300
22
+
23
+ # Default page size for paginated requests
24
+ DEFAULT_PAGE_SIZE = 100
25
+
26
+ # Maximum page size allowed by Jira API
27
+ MAX_PAGE_SIZE = 1000
28
+
29
+ # Base API path for Jira Cloud
30
+ API_BASE_PATH = "/rest/api/3"
31
+
32
+ # Project fields to retrieve from Jira API
33
+ PROJECT_FIELDS = (
34
+ "id",
35
+ "key",
36
+ "name",
37
+ "description",
38
+ "lead",
39
+ "projectCategory",
40
+ "projectTypeKey",
41
+ "simplified",
42
+ "style",
43
+ "favourite",
44
+ "isPrivate",
45
+ "properties",
46
+ "entityId",
47
+ "uuid",
48
+ "insight",
49
+ )
50
+
51
+ # Issue fields to retrieve from Jira API
52
+ ISSUE_FIELDS = (
53
+ "id",
54
+ "key",
55
+ "summary",
56
+ "description",
57
+ "issuetype",
58
+ "status",
59
+ "priority",
60
+ "resolution",
61
+ "assignee",
62
+ "reporter",
63
+ "creator",
64
+ "created",
65
+ "updated",
66
+ "resolutiondate",
67
+ "duedate",
68
+ "components",
69
+ "fixVersions",
70
+ "versions",
71
+ "labels",
72
+ "environment",
73
+ "project",
74
+ "parent",
75
+ "subtasks",
76
+ "issuelinks",
77
+ "votes",
78
+ "watches",
79
+ "worklog",
80
+ "attachments",
81
+ "comment",
82
+ "customfield_*",
83
+ )
84
+
85
+ # User fields to retrieve from Jira API
86
+ USER_FIELDS = (
87
+ "accountId",
88
+ "accountType",
89
+ "emailAddress",
90
+ "displayName",
91
+ "active",
92
+ "timeZone",
93
+ "groups",
94
+ "applicationRoles",
95
+ "expand",
96
+ )
97
+
98
+ # Board fields to retrieve from Jira API (for Agile/Scrum boards)
99
+ BOARD_FIELDS = (
100
+ "id",
101
+ "name",
102
+ "type",
103
+ "location",
104
+ "filter",
105
+ "subQuery",
106
+ )
107
+
108
+ # Sprint fields to retrieve from Jira API
109
+ SPRINT_FIELDS = (
110
+ "id",
111
+ "name",
112
+ "state",
113
+ "startDate",
114
+ "endDate",
115
+ "completeDate",
116
+ "originBoardId",
117
+ "goal",
118
+ )
119
+
120
+ # Issue type fields to retrieve from Jira API
121
+ ISSUE_TYPE_FIELDS = (
122
+ "id",
123
+ "name",
124
+ "description",
125
+ "iconUrl",
126
+ "subtask",
127
+ "avatarId",
128
+ "hierarchyLevel",
129
+ )
130
+
131
+ # Status fields to retrieve from Jira API
132
+ STATUS_FIELDS = (
133
+ "id",
134
+ "name",
135
+ "description",
136
+ "iconUrl",
137
+ "statusCategory",
138
+ )
139
+
140
+ # Priority fields to retrieve from Jira API
141
+ PRIORITY_FIELDS = (
142
+ "id",
143
+ "name",
144
+ "description",
145
+ "iconUrl",
146
+ )
147
+
148
+ # Resolution fields to retrieve from Jira API
149
+ RESOLUTION_FIELDS = (
150
+ "id",
151
+ "name",
152
+ "description",
153
+ )
154
+
155
+ # Version fields to retrieve from Jira API
156
+ VERSION_FIELDS = (
157
+ "id",
158
+ "name",
159
+ "description",
160
+ "archived",
161
+ "released",
162
+ "startDate",
163
+ "releaseDate",
164
+ "overdue",
165
+ "userStartDate",
166
+ "userReleaseDate",
167
+ "project",
168
+ "projectId",
169
+ )
170
+
171
+ # Component fields to retrieve from Jira API
172
+ COMPONENT_FIELDS = (
173
+ "id",
174
+ "name",
175
+ "description",
176
+ "lead",
177
+ "assigneeType",
178
+ "assignee",
179
+ "realAssigneeType",
180
+ "realAssignee",
181
+ "isAssigneeTypeValid",
182
+ "project",
183
+ "projectId",
184
+ )