python-jira-plus 2.0.8__py3-none-any.whl → 3.0.0__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.
@@ -8,7 +8,9 @@ pagination, metadata validation, and more.
8
8
  from dotenv import load_dotenv
9
9
 
10
10
  from python_jira_plus.describe_allowed_value import describe_allowed_value
11
- from python_jira_plus.jira_plus import JiraPlus, ServerType, UrlScheme
11
+ from python_jira_plus.jira_base import JiraBase, UrlScheme
12
+ from python_jira_plus.jira_cloud import JiraCloud
13
+ from python_jira_plus.jira_on_premise import JiraOnPremise
12
14
 
13
15
  load_dotenv()
14
16
 
@@ -27,4 +29,4 @@ BASIC_FIELDS = [
27
29
  "resolution",
28
30
  "parent",
29
31
  ]
30
- __all__ = ["JiraPlus", "ServerType", "UrlScheme", "BASIC_FIELDS"]
32
+ __all__ = ["JiraBase", "JiraCloud", "JiraOnPremise", "UrlScheme", "BASIC_FIELDS"]
@@ -1,18 +1,16 @@
1
- # jira_plus.py
1
+ # jira_base.py
2
2
 
3
3
  import json
4
4
  import logging
5
5
  import os
6
- import time
6
+ from abc import ABC, abstractmethod
7
7
  from enum import Enum
8
8
  from typing import Any
9
9
 
10
- import requests
11
10
  from custom_python_logger import get_logger
12
11
  from jira import JIRA, Issue, JIRAError
13
12
  from jira.client import ResultList
14
- from retrying import retry
15
- from packaging.version import Version
13
+
16
14
  from python_jira_plus.describe_allowed_value import describe_allowed_value
17
15
 
18
16
 
@@ -21,21 +19,16 @@ class UrlScheme(Enum):
21
19
  HTTPS = "https"
22
20
 
23
21
 
24
- class ServerType(Enum):
25
- CLOUD = "cloud"
26
- ON_PREMISE = "on_premise"
27
-
22
+ class JiraBase(ABC):
23
+ use_createmeta: bool
28
24
 
29
- class JiraPlus:
30
25
  def __init__(
31
26
  self,
32
27
  base_url: str | None = None,
33
- server_type: ServerType = ServerType.CLOUD,
34
28
  urllib3_log_level: int = logging.WARNING,
35
29
  jira_username: str | None = None,
36
- jira_token: str | None = None, # Use API token (not PAT)
30
+ jira_token: str | None = None,
37
31
  verify_ssl: bool = True,
38
- sso: bool = False,
39
32
  url_scheme: UrlScheme = UrlScheme.HTTPS,
40
33
  ) -> None:
41
34
  self.logger = get_logger(self.__class__.__name__)
@@ -45,35 +38,28 @@ class JiraPlus:
45
38
  self.jira_token = jira_token or os.getenv("JIRA_TOKEN")
46
39
  self.base_url = base_url or os.getenv("JIRA_BASE_URL")
47
40
  self.url_scheme = url_scheme.value
48
-
49
- self.server_type = server_type
50
41
  self.server = f"{self.url_scheme}://{self.base_url}"
51
42
  self.verify_ssl = verify_ssl
52
- self.sso = sso
53
43
 
54
44
  self.jira_client = self.create_connection()
55
45
  self.check_client_connection()
56
46
  self.server_version = self.get_server_version()
57
- self.use_legacy_api = self._resolve_use_legacy_api()
58
47
 
59
- @retry(stop_max_attempt_number=3, wait_fixed=180000)
60
- def create_connection(self, timeout: int = 580) -> JIRA:
61
- if self.server_type == ServerType.ON_PREMISE and not self.sso:
62
- jira_client = JIRA(
63
- token_auth=self.jira_token, options={"server": self.server, "verify": self.verify_ssl}, timeout=timeout
64
- )
65
- else:
66
- jira_client = JIRA(
67
- basic_auth=(self.jira_username, self.jira_token),
68
- options={"server": self.server, "verify": self.verify_ssl},
69
- timeout=timeout,
70
- )
48
+ @abstractmethod
49
+ def create_connection(self, timeout: int = 580) -> JIRA: ...
50
+
51
+ @abstractmethod
52
+ def _paginate(
53
+ self,
54
+ query: str,
55
+ max_results: int,
56
+ specific_fields: str | list[str],
57
+ jira_err_limit: int,
58
+ json_result: bool,
59
+ ) -> ResultList[Issue] | list: ...
71
60
 
72
- if jira_client.projects():
73
- self.logger.info("Jira Connection Successful")
74
- return jira_client
75
- self.logger.error("Jira Connection Failed")
76
- raise JIRAError("Jira Connection Failed")
61
+ @abstractmethod
62
+ def _format_assignee(self, assignee: str) -> dict: ...
77
63
 
78
64
  def close_connection(self) -> None:
79
65
  try:
@@ -82,7 +68,6 @@ class JiraPlus:
82
68
  self.logger.exception(f"JIRAError \n{err}")
83
69
  except Exception as err:
84
70
  self.logger.exception(f"Exception \n{err}")
85
-
86
71
  self.check_client_connection()
87
72
 
88
73
  def check_client_connection(self) -> bool:
@@ -98,6 +83,9 @@ class JiraPlus:
98
83
  self.logger.exception(f"Exception \n{err}")
99
84
  return False
100
85
 
86
+ def get_server_version(self) -> tuple[int, ...]:
87
+ return tuple(self.jira_client.server_info()["versionNumbers"])
88
+
101
89
  def get_issue_by_key(
102
90
  self, key: str, specific_fields: str | list[str] = "*all", json_result: bool = True
103
91
  ) -> None | Issue | dict[str, Any]:
@@ -120,12 +108,8 @@ class JiraPlus:
120
108
  specific_fields: str | list[str] = "*all",
121
109
  jira_err_count: int = 3,
122
110
  json_result: bool = True,
123
- ) -> ResultList[Issue] | dict | None:
124
- if self.server_type == ServerType.ON_PREMISE:
125
- _paginate_query_func = self._paginate_query
126
- else:
127
- _paginate_query_func = self._paginate_query_new
128
- return _paginate_query_func(
111
+ ) -> ResultList[Issue] | list:
112
+ return self._paginate(
129
113
  query=query,
130
114
  max_results=max_results,
131
115
  specific_fields=specific_fields,
@@ -133,107 +117,26 @@ class JiraPlus:
133
117
  json_result=json_result,
134
118
  )
135
119
 
136
- def _paginate_query_new(
137
- self,
138
- query: str,
139
- max_results: int,
140
- specific_fields: str | list[str],
141
- jira_err_limit: int,
142
- json_result: bool,
143
- ) -> ResultList[Issue] | list:
144
- all_issues = []
145
- retries = 0
146
- page = None
147
- while True:
148
- try:
149
- next_token = None
150
- if page is not None:
151
- next_token = page.get('nextPageToken') if json_result else page.nextPageToken
152
-
153
- self.logger.debug(f"Fetching issues from startAt={len(all_issues)}")
154
- page = self.jira_client.enhanced_search_issues(
155
- jql_str=query,
156
- nextPageToken=next_token,
157
- maxResults=max_results,
158
- fields=specific_fields,
159
- json_result=json_result,
160
- )
161
- if not page:
162
- break
163
-
164
- all_issues += page["issues"] if json_result else page
165
- is_last = page['isLast'] if json_result else page.isLast
166
- if is_last:
167
- break
168
- retries = 0 # reset on success
169
- except JIRAError as err:
170
- if err.status_code == 400:
171
- self.logger.error(f"Bad Request: {err.text}")
172
- raise JIRAError(f"Bad Request: {err.text}") from err
173
- retries += 1
174
- self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
175
- if retries >= jira_err_limit:
176
- self.logger.exception("Max retries exceeded.")
177
- return []
178
- time.sleep(300 if retries == jira_err_limit - 1 else 90)
179
- return all_issues
180
-
181
- def _paginate_query(
182
- self,
183
- query: str,
184
- max_results: int,
185
- specific_fields: str | list[str],
186
- jira_err_limit: int,
187
- json_result: bool,
188
- ) -> ResultList[Issue] | list:
189
- start_at = 0
190
- all_issues = []
191
- retries = 0
192
- while True: # pylint: disable=W0149
193
- try:
194
- self.logger.debug(f"Fetching issues from startAt={start_at}")
195
- page = self.jira_client.search_issues(
196
- jql_str=query,
197
- startAt=start_at,
198
- maxResults=max_results,
199
- fields=specific_fields,
200
- json_result=json_result,
201
- )
202
- if not page:
203
- break
204
-
205
- all_issues += page["issues"] if json_result else page
206
- if json_result:
207
- start_at += page["maxResults"]
208
- else:
209
- start_at += page.maxResults
210
- if start_at >= max_results:
211
- break
212
- retries = 0 # reset on success
213
- except JIRAError as err:
214
- if err.status_code == 400:
215
- self.logger.error(f"Bad Request: {err.text}")
216
- raise JIRAError(f"Bad Request: {err.text}") from err
217
- retries += 1
218
- self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
219
- if retries >= jira_err_limit:
220
- self.logger.exception("Max retries exceeded.")
221
- return []
222
- time.sleep(300 if retries == jira_err_limit - 1 else 90)
223
- return all_issues
224
-
225
- def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
120
+ def _fetch_via_createmeta(self, project_key: str, issue_type: str) -> dict:
226
121
  meta = self.jira_client.createmeta(
227
122
  projectKeys=project_key, issuetypeNames=issue_type, expand="projects.issuetypes.fields"
228
123
  )
229
-
230
124
  if not (project_meta := next((p for p in meta["projects"] if p["key"] == project_key), None)):
231
125
  raise ValueError(f"Project {project_key} not found in metadata")
232
-
233
126
  issue_meta = next((it for it in project_meta["issuetypes"] if it["name"].lower() == issue_type.lower()), None)
234
127
  if not issue_meta:
235
128
  raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
236
- return meta, issue_meta
129
+ return issue_meta["fields"]
130
+
131
+ def _fetch_via_issue_types_api(self, project_key: str, issue_type: str) -> dict:
132
+ issue_types = {t.name: t for t in self.jira_client.project_issue_types(project_key)}
133
+ field_list = self.jira_client.project_issue_fields(project_key, issue_types[issue_type].id)
134
+ return {f.fieldId: f.raw for f in field_list}
135
+
136
+ def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
137
+ if self.use_createmeta:
138
+ return self._fetch_via_createmeta(project_key=project_key, issue_type=issue_type)
139
+ return self._fetch_via_issue_types_api(project_key=project_key, issue_type=issue_type)
237
140
 
238
141
  def get_allowed_values(
239
142
  self,
@@ -241,28 +144,20 @@ class JiraPlus:
241
144
  issue_type: str,
242
145
  field_id_or_name: str,
243
146
  ) -> list | None:
244
- """
245
- Returns the allowed values for a specific field in a project + issue type context.
246
- """
147
+ """Returns the allowed values for a specific field in a project + issue type context."""
247
148
  fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
248
-
249
- # Try to find by ID or fallback to case-insensitive name
250
149
  if not (field := fields_metadata.get(field_id_or_name)):
251
- # Try matching by name (case-insensitive)
252
150
  for _, fmeta in fields_metadata.items():
253
151
  if fmeta["name"].lower() == field_id_or_name.lower():
254
152
  field = fmeta
255
153
  break
256
-
257
154
  if not field:
258
155
  raise ValueError(
259
156
  f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'"
260
157
  )
261
-
262
158
  if not (allowed := field.get("allowedValues")):
263
159
  self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
264
160
  return None
265
-
266
161
  return allowed
267
162
 
268
163
  @staticmethod
@@ -272,36 +167,13 @@ class JiraPlus:
272
167
  allowed_values = allowed_names | allowed_keys
273
168
  return describe_allowed_value(value, allowed_values=allowed_values)
274
169
 
275
- def get_server_version(self) -> tuple[int, ...]:
276
- return tuple(self.jira_client.server_info()["versionNumbers"])
277
-
278
- def _resolve_use_legacy_api(self) -> bool:
279
- if self.server_type == ServerType.ON_PREMISE:
280
- use_legacy = Version(".".join(str(x) for x in self.server_version)) < Version("9.0.0")
281
- self.logger.debug(f"Using {'legacy createmeta' if use_legacy else 'project_issue_types'} API (ON_PREMISE {self.server_version})")
282
- return use_legacy
283
- self.logger.debug("Using legacy createmeta API (Cloud)")
284
- return True
285
-
286
- def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
287
- if self.use_legacy_api:
288
- _, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
289
- return issue_meta["fields"]
290
- _project_issue_types = self.jira_client.project_issue_types(project_key)
291
- _issue_types = {t.name: t for t in _project_issue_types}
292
- field_list = self.jira_client.project_issue_fields(project_key, _issue_types[issue_type].id)
293
- return {f.fieldId: f.raw for f in field_list}
294
-
295
- def validate_fields( # pylint: disable=R1260
296
- self, project_key: str, issue_type: str, fields: dict
297
- ) -> None:
170
+ def validate_fields(self, project_key: str, issue_type: str, fields: dict) -> None: # pylint: disable=R1260
298
171
  fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
299
172
  for custom_field, value in fields.items():
300
173
  if custom_field not in fields_metadata:
301
174
  raise ValueError(
302
175
  f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
303
176
  )
304
-
305
177
  if allowed_value := fields_metadata[custom_field].get("allowedValues"):
306
178
  if not self._is_value_allowed(value=value, allowed_values=allowed_value):
307
179
  raise ValueError(
@@ -309,7 +181,6 @@ class JiraPlus:
309
181
  f"with issue type '{issue_type}'."
310
182
  f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
311
183
  )
312
-
313
184
  field_type = fields_metadata[custom_field]["schema"]["type"]
314
185
  if field_type == "string" and not isinstance(value, str):
315
186
  raise ValueError(f"Field '{custom_field}' must be a string")
@@ -323,7 +194,6 @@ class JiraPlus:
323
194
  raise ValueError(f"Field '{custom_field}' must be an object")
324
195
  if field_type == "issuetype" and not isinstance(value, dict):
325
196
  raise ValueError(f"Field '{custom_field}' must be an issue type")
326
- # Add more field type checks as needed
327
197
 
328
198
  def create_issue(
329
199
  self,
@@ -334,21 +204,17 @@ class JiraPlus:
334
204
  assignee: str | None = None,
335
205
  custom_fields: dict | None = None,
336
206
  ) -> Issue | None:
337
- issue_fields = {
207
+ issue_fields: dict[str, Any] = {
338
208
  "project": {"key": project_key},
339
209
  "issuetype": {"name": issue_type},
340
210
  "summary": summary,
341
211
  "description": description,
342
212
  }
343
213
  if assignee:
344
- issue_fields["assignee"] = (
345
- {"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
346
- )
347
-
214
+ issue_fields["assignee"] = self._format_assignee(assignee)
348
215
  if custom_fields:
349
216
  issue_fields = issue_fields | custom_fields
350
217
  self.validate_fields(project_key=project_key, issue_type=issue_type, fields=issue_fields)
351
-
352
218
  issue = self.jira_client.create_issue(fields=issue_fields)
353
219
  self.logger.info(f"Issue {issue.key} created successfully.")
354
220
  return issue
@@ -359,7 +225,6 @@ class JiraPlus:
359
225
  self.validate_fields(
360
226
  project_key=issue.fields.project.key, issue_type=issue.fields.issuetype.name, fields=fields_to_update
361
227
  )
362
-
363
228
  issue.update(fields=fields_to_update)
364
229
  self.logger.info(f"Issue {issue.key} updated successfully.")
365
230
  return issue
@@ -398,7 +263,6 @@ class JiraPlus:
398
263
  available_link_types = self._get_available_link_types()
399
264
  if link_type not in available_link_types:
400
265
  raise ValueError(f"Invalid link type '{link_type}'. Available link types: {available_link_types}")
401
-
402
266
  try:
403
267
  self.jira_client.create_issue_link(
404
268
  type=link_type,
@@ -438,12 +302,10 @@ class JiraPlus:
438
302
  f"\nAvailable transitions: {transition_names}"
439
303
  )
440
304
  return False
441
-
442
305
  transition_id = next((t["id"] for t in transitions if t["name"].lower() == transition_name.lower()), None)
443
306
  if not transition_id:
444
307
  self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
445
308
  return False
446
-
447
309
  self.jira_client.transition_issue(
448
310
  issue=issue_key, transition=transition_id, fields=fields or {}, comment=comment or None
449
311
  )
@@ -455,21 +317,3 @@ class JiraPlus:
455
317
  except Exception as err:
456
318
  self.logger.exception(f"Unexpected error during transition of {issue_key}: {err}")
457
319
  return False
458
-
459
- def get_account_data(self, user_mail: str) -> list[dict]:
460
- response = requests.get(
461
- f"{self.server}/rest/api/3/user/search",
462
- params={"query": user_mail},
463
- auth=(self.jira_username, self.jira_token),
464
- timeout=30,
465
- )
466
- response.raise_for_status()
467
- return response.json()
468
-
469
- def get_account_id(self, user_mail: str) -> str:
470
- data = self.get_account_data(user_mail=user_mail)
471
- return data[0]["accountId"]
472
-
473
- def get_account_display_name(self, user_display_name: str) -> str:
474
- data = self.get_account_data(user_mail=user_display_name)
475
- return data[0]["displayName"]
@@ -0,0 +1,111 @@
1
+ # jira_cloud.py
2
+
3
+ import logging
4
+ import time
5
+
6
+ import requests
7
+ from jira import JIRA, Issue, JIRAError
8
+ from jira.client import ResultList
9
+ from retrying import retry
10
+
11
+ from python_jira_plus.jira_base import JiraBase, UrlScheme
12
+
13
+
14
+ class JiraCloud(JiraBase):
15
+ use_createmeta = True
16
+
17
+ def __init__(
18
+ self,
19
+ base_url: str | None = None,
20
+ urllib3_log_level: int = logging.WARNING,
21
+ jira_username: str | None = None,
22
+ jira_token: str | None = None,
23
+ verify_ssl: bool = True,
24
+ url_scheme: UrlScheme = UrlScheme.HTTPS,
25
+ ) -> None:
26
+ super().__init__(
27
+ base_url=base_url,
28
+ urllib3_log_level=urllib3_log_level,
29
+ jira_username=jira_username,
30
+ jira_token=jira_token,
31
+ verify_ssl=verify_ssl,
32
+ url_scheme=url_scheme,
33
+ )
34
+
35
+ @retry(stop_max_attempt_number=3, wait_fixed=180000)
36
+ def create_connection(self, timeout: int = 580) -> JIRA:
37
+ jira_client = JIRA(
38
+ basic_auth=(self.jira_username, self.jira_token),
39
+ options={"server": self.server, "verify": self.verify_ssl},
40
+ timeout=timeout,
41
+ )
42
+ if jira_client.projects():
43
+ self.logger.info("Jira Connection Successful")
44
+ return jira_client
45
+ self.logger.error("Jira Connection Failed")
46
+ raise JIRAError("Jira Connection Failed")
47
+
48
+ def _format_assignee(self, assignee: str) -> dict:
49
+ return {"accountId": assignee}
50
+
51
+ def _paginate(
52
+ self,
53
+ query: str,
54
+ max_results: int,
55
+ specific_fields: str | list[str],
56
+ jira_err_limit: int,
57
+ json_result: bool,
58
+ ) -> ResultList[Issue] | list:
59
+ all_issues = []
60
+ retries = 0
61
+ page = None
62
+ while True: # pylint: disable=while-used
63
+ try:
64
+ next_token = None
65
+ if page is not None:
66
+ next_token = page.get("nextPageToken") if json_result else page.nextPageToken
67
+
68
+ self.logger.debug(f"Fetching issues from startAt={len(all_issues)}")
69
+ page = self.jira_client.enhanced_search_issues(
70
+ jql_str=query,
71
+ nextPageToken=next_token,
72
+ maxResults=max_results,
73
+ fields=specific_fields,
74
+ json_result=json_result,
75
+ )
76
+ if not page:
77
+ break
78
+
79
+ all_issues += page["issues"] if json_result else page
80
+ if page["isLast"] if json_result else page.isLast:
81
+ break
82
+ retries = 0
83
+ except JIRAError as err:
84
+ if err.status_code == 400:
85
+ self.logger.error(f"Bad Request: {err.text}")
86
+ raise JIRAError(f"Bad Request: {err.text}") from err
87
+ retries += 1
88
+ self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
89
+ if retries >= jira_err_limit:
90
+ self.logger.exception("Max retries exceeded.")
91
+ return []
92
+ time.sleep(300 if retries == jira_err_limit - 1 else 90)
93
+ return all_issues
94
+
95
+ def get_account_data(self, user_mail: str) -> list[dict]:
96
+ response = requests.get(
97
+ f"{self.server}/rest/api/3/user/search",
98
+ params={"query": user_mail},
99
+ auth=(self.jira_username, self.jira_token),
100
+ timeout=30,
101
+ )
102
+ response.raise_for_status()
103
+ return response.json()
104
+
105
+ def get_account_id(self, user_mail: str) -> str:
106
+ data = self.get_account_data(user_mail=user_mail)
107
+ return data[0]["accountId"]
108
+
109
+ def get_account_display_name(self, user_display_name: str) -> str:
110
+ data = self.get_account_data(user_mail=user_display_name)
111
+ return data[0]["displayName"]
@@ -0,0 +1,98 @@
1
+ # jira_on_premise.py
2
+
3
+ import logging
4
+ import time
5
+
6
+ from jira import JIRA, Issue, JIRAError
7
+ from jira.client import ResultList
8
+ from packaging.version import Version
9
+ from retrying import retry
10
+
11
+ from python_jira_plus.jira_base import JiraBase, UrlScheme
12
+
13
+
14
+ class JiraOnPremise(JiraBase):
15
+ def __init__(
16
+ self,
17
+ base_url: str | None = None,
18
+ urllib3_log_level: int = logging.WARNING,
19
+ jira_username: str | None = None,
20
+ jira_token: str | None = None,
21
+ verify_ssl: bool = True,
22
+ sso: bool = False,
23
+ url_scheme: UrlScheme = UrlScheme.HTTPS,
24
+ ) -> None:
25
+ self.sso = sso # must be set before super().__init__ — create_connection reads it
26
+ super().__init__(
27
+ base_url=base_url,
28
+ urllib3_log_level=urllib3_log_level,
29
+ jira_username=jira_username,
30
+ jira_token=jira_token,
31
+ verify_ssl=verify_ssl,
32
+ url_scheme=url_scheme,
33
+ )
34
+ use_legacy = Version(".".join(str(x) for x in self.server_version)) < Version("9.0.0")
35
+ self.logger.debug(
36
+ f"Using {'legacy createmeta' if use_legacy else 'project_issue_types'} API "
37
+ f"(ON_PREMISE {self.server_version})"
38
+ )
39
+ self.use_createmeta = use_legacy
40
+
41
+ @retry(stop_max_attempt_number=3, wait_fixed=180000)
42
+ def create_connection(self, timeout: int = 580) -> JIRA:
43
+ auth = (
44
+ {"token_auth": self.jira_token} if not self.sso else {"basic_auth": (self.jira_username, self.jira_token)}
45
+ )
46
+ jira_client = JIRA(**auth, options={"server": self.server, "verify": self.verify_ssl}, timeout=timeout)
47
+ if jira_client.projects():
48
+ self.logger.info("Jira Connection Successful")
49
+ return jira_client
50
+ self.logger.error("Jira Connection Failed")
51
+ raise JIRAError("Jira Connection Failed")
52
+
53
+ def _format_assignee(self, assignee: str) -> dict:
54
+ return {"name": assignee}
55
+
56
+ def _paginate(
57
+ self,
58
+ query: str,
59
+ max_results: int,
60
+ specific_fields: str | list[str],
61
+ jira_err_limit: int,
62
+ json_result: bool,
63
+ ) -> ResultList[Issue] | list:
64
+ start_at = 0
65
+ all_issues = []
66
+ retries = 0
67
+ while True: # pylint: disable=while-used
68
+ try:
69
+ self.logger.debug(f"Fetching issues from startAt={start_at}")
70
+ page = self.jira_client.search_issues(
71
+ jql_str=query,
72
+ startAt=start_at,
73
+ maxResults=max_results,
74
+ fields=specific_fields,
75
+ json_result=json_result,
76
+ )
77
+ if not page:
78
+ break
79
+
80
+ all_issues += page["issues"] if json_result else page
81
+ if json_result:
82
+ start_at += page["maxResults"]
83
+ else:
84
+ start_at += page.maxResults
85
+ if start_at >= max_results:
86
+ break
87
+ retries = 0
88
+ except JIRAError as err:
89
+ if err.status_code == 400:
90
+ self.logger.error(f"Bad Request: {err.text}")
91
+ raise JIRAError(f"Bad Request: {err.text}") from err
92
+ retries += 1
93
+ self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
94
+ if retries >= jira_err_limit:
95
+ self.logger.exception("Max retries exceeded.")
96
+ return []
97
+ time.sleep(300 if retries == jira_err_limit - 1 else 90)
98
+ return all_issues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jira-plus
3
- Version: 2.0.8
3
+ Version: 3.0.0
4
4
  Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation.
5
5
  Author: Avi Zaguri
6
6
  License: MIT
@@ -0,0 +1,10 @@
1
+ python_jira_plus/__init__.py,sha256=HMlPp5HOfdEO3VYE7Zc2qfhLsIPpPJ9wQaUojhCOp0k,729
2
+ python_jira_plus/describe_allowed_value.py,sha256=fWfq0DkvJ2zmNIUXPH5rO5OgcY69bUqBIfTPQyxBt5E,1287
3
+ python_jira_plus/jira_base.py,sha256=OKRm5ArLdQRLoQQuIz7rgKv_jKb2S23ADM_RSKghxjw,13839
4
+ python_jira_plus/jira_cloud.py,sha256=pD5w2i3g7FpK-xk8dT9pHsNanAm257PlRt3O2glAFvA,3879
5
+ python_jira_plus/jira_on_premise.py,sha256=NWWOgKmX5M8h58m1j4Rr3MRWK1vRIH2Itgae_76ezAA,3633
6
+ python_jira_plus-3.0.0.dist-info/licenses/LICENSE,sha256=cSikHY6SZFsPZSBizCDAJ0-Bjjzxt-JtX6TVbKxwimo,1067
7
+ python_jira_plus-3.0.0.dist-info/METADATA,sha256=A81DGwl6RL2VC70xOkibt1VtaIs1b6P1j3g47eu3OVo,4285
8
+ python_jira_plus-3.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ python_jira_plus-3.0.0.dist-info/top_level.txt,sha256=3DxBdmf_naggoqDHSvLstG_GX3bW2XXYYorBDybBUiA,17
10
+ python_jira_plus-3.0.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- python_jira_plus/__init__.py,sha256=BUKjGxQfkxWyZs-mKZs2bAgGnsYHQ4uOFjuB8MPmhfg,616
2
- python_jira_plus/describe_allowed_value.py,sha256=fWfq0DkvJ2zmNIUXPH5rO5OgcY69bUqBIfTPQyxBt5E,1287
3
- python_jira_plus/jira_plus.py,sha256=TNJkwFdDbkaVrsEaO3kBRGHi_GDFVhRcpoX6bAgyRTs,19568
4
- python_jira_plus-2.0.8.dist-info/licenses/LICENSE,sha256=cSikHY6SZFsPZSBizCDAJ0-Bjjzxt-JtX6TVbKxwimo,1067
5
- python_jira_plus-2.0.8.dist-info/METADATA,sha256=AdZUwcmuzp3qtoFVuaB1En8QQqjKxFz6_3U99X6G-o8,4285
6
- python_jira_plus-2.0.8.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
- python_jira_plus-2.0.8.dist-info/top_level.txt,sha256=3DxBdmf_naggoqDHSvLstG_GX3bW2XXYYorBDybBUiA,17
8
- python_jira_plus-2.0.8.dist-info/RECORD,,