python-jira-plus 2.0.8__py3-none-any.whl → 3.1.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"]
@@ -0,0 +1 @@
1
+ LOGGER_NAME = "pytest_depends_on"
@@ -1,18 +1,17 @@
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
+
14
+ from python_jira_plus.const import LOGGER_NAME
16
15
  from python_jira_plus.describe_allowed_value import describe_allowed_value
17
16
 
18
17
 
@@ -21,59 +20,47 @@ class UrlScheme(Enum):
21
20
  HTTPS = "https"
22
21
 
23
22
 
24
- class ServerType(Enum):
25
- CLOUD = "cloud"
26
- ON_PREMISE = "on_premise"
27
-
23
+ class JiraBase(ABC):
24
+ use_createmeta: bool
28
25
 
29
- class JiraPlus:
30
26
  def __init__(
31
27
  self,
32
28
  base_url: str | None = None,
33
- server_type: ServerType = ServerType.CLOUD,
34
29
  urllib3_log_level: int = logging.WARNING,
35
30
  jira_username: str | None = None,
36
- jira_token: str | None = None, # Use API token (not PAT)
31
+ jira_token: str | None = None,
37
32
  verify_ssl: bool = True,
38
- sso: bool = False,
39
33
  url_scheme: UrlScheme = UrlScheme.HTTPS,
40
34
  ) -> None:
41
- self.logger = get_logger(self.__class__.__name__)
35
+ self.logger = get_logger(LOGGER_NAME)
42
36
  get_logger("urllib3").setLevel(urllib3_log_level)
43
37
 
44
38
  self.jira_username = jira_username or os.getenv("JIRA_USER_NAME")
45
39
  self.jira_token = jira_token or os.getenv("JIRA_TOKEN")
46
40
  self.base_url = base_url or os.getenv("JIRA_BASE_URL")
47
41
  self.url_scheme = url_scheme.value
48
-
49
- self.server_type = server_type
50
42
  self.server = f"{self.url_scheme}://{self.base_url}"
51
43
  self.verify_ssl = verify_ssl
52
- self.sso = sso
53
44
 
54
45
  self.jira_client = self.create_connection()
55
46
  self.check_client_connection()
56
47
  self.server_version = self.get_server_version()
57
- self.use_legacy_api = self._resolve_use_legacy_api()
58
48
 
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
- )
49
+ @abstractmethod
50
+ def create_connection(self, timeout: int = 580) -> JIRA: ...
51
+
52
+ @abstractmethod
53
+ def _paginate(
54
+ self,
55
+ query: str,
56
+ max_results: int,
57
+ specific_fields: str | list[str],
58
+ jira_err_limit: int,
59
+ json_result: bool,
60
+ ) -> ResultList[Issue] | list: ...
71
61
 
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")
62
+ @abstractmethod
63
+ def _format_assignee(self, assignee: str) -> dict: ...
77
64
 
78
65
  def close_connection(self) -> None:
79
66
  try:
@@ -82,7 +69,6 @@ class JiraPlus:
82
69
  self.logger.exception(f"JIRAError \n{err}")
83
70
  except Exception as err:
84
71
  self.logger.exception(f"Exception \n{err}")
85
-
86
72
  self.check_client_connection()
87
73
 
88
74
  def check_client_connection(self) -> bool:
@@ -98,6 +84,9 @@ class JiraPlus:
98
84
  self.logger.exception(f"Exception \n{err}")
99
85
  return False
100
86
 
87
+ def get_server_version(self) -> tuple[int, ...]:
88
+ return tuple(self.jira_client.server_info()["versionNumbers"])
89
+
101
90
  def get_issue_by_key(
102
91
  self, key: str, specific_fields: str | list[str] = "*all", json_result: bool = True
103
92
  ) -> None | Issue | dict[str, Any]:
@@ -120,12 +109,8 @@ class JiraPlus:
120
109
  specific_fields: str | list[str] = "*all",
121
110
  jira_err_count: int = 3,
122
111
  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(
112
+ ) -> ResultList[Issue] | list:
113
+ return self._paginate(
129
114
  query=query,
130
115
  max_results=max_results,
131
116
  specific_fields=specific_fields,
@@ -133,107 +118,26 @@ class JiraPlus:
133
118
  json_result=json_result,
134
119
  )
135
120
 
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]:
121
+ def _fetch_via_createmeta(self, project_key: str, issue_type: str) -> dict:
226
122
  meta = self.jira_client.createmeta(
227
123
  projectKeys=project_key, issuetypeNames=issue_type, expand="projects.issuetypes.fields"
228
124
  )
229
-
230
125
  if not (project_meta := next((p for p in meta["projects"] if p["key"] == project_key), None)):
231
126
  raise ValueError(f"Project {project_key} not found in metadata")
232
-
233
127
  issue_meta = next((it for it in project_meta["issuetypes"] if it["name"].lower() == issue_type.lower()), None)
234
128
  if not issue_meta:
235
129
  raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
236
- return meta, issue_meta
130
+ return issue_meta["fields"]
131
+
132
+ def _fetch_via_issue_types_api(self, project_key: str, issue_type: str) -> dict:
133
+ issue_types = {t.name: t for t in self.jira_client.project_issue_types(project_key)}
134
+ field_list = self.jira_client.project_issue_fields(project_key, issue_types[issue_type].id)
135
+ return {f.fieldId: f.raw for f in field_list}
136
+
137
+ def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
138
+ if self.use_createmeta:
139
+ return self._fetch_via_createmeta(project_key=project_key, issue_type=issue_type)
140
+ return self._fetch_via_issue_types_api(project_key=project_key, issue_type=issue_type)
237
141
 
238
142
  def get_allowed_values(
239
143
  self,
@@ -241,28 +145,20 @@ class JiraPlus:
241
145
  issue_type: str,
242
146
  field_id_or_name: str,
243
147
  ) -> list | None:
244
- """
245
- Returns the allowed values for a specific field in a project + issue type context.
246
- """
148
+ """Returns the allowed values for a specific field in a project + issue type context."""
247
149
  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
150
  if not (field := fields_metadata.get(field_id_or_name)):
251
- # Try matching by name (case-insensitive)
252
151
  for _, fmeta in fields_metadata.items():
253
152
  if fmeta["name"].lower() == field_id_or_name.lower():
254
153
  field = fmeta
255
154
  break
256
-
257
155
  if not field:
258
156
  raise ValueError(
259
157
  f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'"
260
158
  )
261
-
262
159
  if not (allowed := field.get("allowedValues")):
263
160
  self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
264
161
  return None
265
-
266
162
  return allowed
267
163
 
268
164
  @staticmethod
@@ -272,36 +168,13 @@ class JiraPlus:
272
168
  allowed_values = allowed_names | allowed_keys
273
169
  return describe_allowed_value(value, allowed_values=allowed_values)
274
170
 
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:
171
+ def validate_fields(self, project_key: str, issue_type: str, fields: dict) -> None: # pylint: disable=R1260
298
172
  fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
299
173
  for custom_field, value in fields.items():
300
174
  if custom_field not in fields_metadata:
301
175
  raise ValueError(
302
176
  f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
303
177
  )
304
-
305
178
  if allowed_value := fields_metadata[custom_field].get("allowedValues"):
306
179
  if not self._is_value_allowed(value=value, allowed_values=allowed_value):
307
180
  raise ValueError(
@@ -309,7 +182,6 @@ class JiraPlus:
309
182
  f"with issue type '{issue_type}'."
310
183
  f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
311
184
  )
312
-
313
185
  field_type = fields_metadata[custom_field]["schema"]["type"]
314
186
  if field_type == "string" and not isinstance(value, str):
315
187
  raise ValueError(f"Field '{custom_field}' must be a string")
@@ -323,7 +195,6 @@ class JiraPlus:
323
195
  raise ValueError(f"Field '{custom_field}' must be an object")
324
196
  if field_type == "issuetype" and not isinstance(value, dict):
325
197
  raise ValueError(f"Field '{custom_field}' must be an issue type")
326
- # Add more field type checks as needed
327
198
 
328
199
  def create_issue(
329
200
  self,
@@ -334,21 +205,17 @@ class JiraPlus:
334
205
  assignee: str | None = None,
335
206
  custom_fields: dict | None = None,
336
207
  ) -> Issue | None:
337
- issue_fields = {
208
+ issue_fields: dict[str, Any] = {
338
209
  "project": {"key": project_key},
339
210
  "issuetype": {"name": issue_type},
340
211
  "summary": summary,
341
212
  "description": description,
342
213
  }
343
214
  if assignee:
344
- issue_fields["assignee"] = (
345
- {"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
346
- )
347
-
215
+ issue_fields["assignee"] = self._format_assignee(assignee)
348
216
  if custom_fields:
349
217
  issue_fields = issue_fields | custom_fields
350
218
  self.validate_fields(project_key=project_key, issue_type=issue_type, fields=issue_fields)
351
-
352
219
  issue = self.jira_client.create_issue(fields=issue_fields)
353
220
  self.logger.info(f"Issue {issue.key} created successfully.")
354
221
  return issue
@@ -359,7 +226,6 @@ class JiraPlus:
359
226
  self.validate_fields(
360
227
  project_key=issue.fields.project.key, issue_type=issue.fields.issuetype.name, fields=fields_to_update
361
228
  )
362
-
363
229
  issue.update(fields=fields_to_update)
364
230
  self.logger.info(f"Issue {issue.key} updated successfully.")
365
231
  return issue
@@ -398,7 +264,6 @@ class JiraPlus:
398
264
  available_link_types = self._get_available_link_types()
399
265
  if link_type not in available_link_types:
400
266
  raise ValueError(f"Invalid link type '{link_type}'. Available link types: {available_link_types}")
401
-
402
267
  try:
403
268
  self.jira_client.create_issue_link(
404
269
  type=link_type,
@@ -438,12 +303,10 @@ class JiraPlus:
438
303
  f"\nAvailable transitions: {transition_names}"
439
304
  )
440
305
  return False
441
-
442
306
  transition_id = next((t["id"] for t in transitions if t["name"].lower() == transition_name.lower()), None)
443
307
  if not transition_id:
444
308
  self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
445
309
  return False
446
-
447
310
  self.jira_client.transition_issue(
448
311
  issue=issue_key, transition=transition_id, fields=fields or {}, comment=comment or None
449
312
  )
@@ -455,21 +318,3 @@ class JiraPlus:
455
318
  except Exception as err:
456
319
  self.logger.exception(f"Unexpected error during transition of {issue_key}: {err}")
457
320
  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.1.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
@@ -16,7 +16,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: custom-python-logger>=2.0.10
19
+ Requires-Dist: custom-python-logger>=4.0.0
20
20
  Requires-Dist: python-dotenv>=1.0.0
21
21
  Requires-Dist: jira>=3.10.5
22
22
  Requires-Dist: packaging>=26.2
@@ -0,0 +1,11 @@
1
+ python_jira_plus/__init__.py,sha256=HMlPp5HOfdEO3VYE7Zc2qfhLsIPpPJ9wQaUojhCOp0k,729
2
+ python_jira_plus/const.py,sha256=lS3kH5jzxhNlJf_jfvH3bc98jQGmpnHoDkJGdj7jLzI,34
3
+ python_jira_plus/describe_allowed_value.py,sha256=fWfq0DkvJ2zmNIUXPH5rO5OgcY69bUqBIfTPQyxBt5E,1287
4
+ python_jira_plus/jira_base.py,sha256=pWLm19YIp8ususNSqIAl8rkvFvhYyg0Cn-W0aZZ64cQ,13874
5
+ python_jira_plus/jira_cloud.py,sha256=pD5w2i3g7FpK-xk8dT9pHsNanAm257PlRt3O2glAFvA,3879
6
+ python_jira_plus/jira_on_premise.py,sha256=NWWOgKmX5M8h58m1j4Rr3MRWK1vRIH2Itgae_76ezAA,3633
7
+ python_jira_plus-3.1.0.dist-info/licenses/LICENSE,sha256=cSikHY6SZFsPZSBizCDAJ0-Bjjzxt-JtX6TVbKxwimo,1067
8
+ python_jira_plus-3.1.0.dist-info/METADATA,sha256=aOiGFJdWuzpzw7URve9c2AVJ_vNVTvAgferlIpdQqr0,4284
9
+ python_jira_plus-3.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ python_jira_plus-3.1.0.dist-info/top_level.txt,sha256=3DxBdmf_naggoqDHSvLstG_GX3bW2XXYYorBDybBUiA,17
11
+ python_jira_plus-3.1.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,,