python-jira-plus 1.0.14__tar.gz → 2.0.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jira-plus
3
- Version: 1.0.14
3
+ Version: 2.0.1
4
4
  Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation.
5
5
  Home-page: https://github.com/aviz92/python-jira-plus
6
6
  Author: Avi Zaguri
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: setuptools
13
13
  Requires-Dist: wheel
14
14
  Requires-Dist: dotenv
15
+ Requires-Dist: pre-commit
15
16
  Requires-Dist: custom-python-logger
16
17
  Requires-Dist: pytest
17
18
  Requires-Dist: requests
@@ -1,5 +1,3 @@
1
- # python-jira-plus
2
-
3
1
  """
4
2
  Python JIRA Plus
5
3
 
@@ -8,26 +6,25 @@ pagination, metadata validation, and more.
8
6
  """
9
7
 
10
8
  from dotenv import load_dotenv
11
- load_dotenv()
12
9
 
13
- from python_jira_plus.jira_plus import JiraPlus, ServerType
14
10
  from python_jira_plus.describe_allowed_value import describe_allowed_value
11
+ from python_jira_plus.jira_plus import JiraPlus, ServerType
15
12
 
16
13
  load_dotenv()
17
14
 
18
15
  BASIC_FIELDS = [
19
- 'created',
20
- 'updated',
21
- 'issuetype',
22
- 'project',
23
- 'status',
24
- 'summary',
25
- 'description',
26
- 'reporter',
27
- 'assignee',
28
- 'priority',
29
- 'labels',
30
- 'resolution',
31
- 'parent'
16
+ "created",
17
+ "updated",
18
+ "issuetype",
19
+ "project",
20
+ "status",
21
+ "summary",
22
+ "description",
23
+ "reporter",
24
+ "assignee",
25
+ "priority",
26
+ "labels",
27
+ "resolution",
28
+ "parent",
32
29
  ]
33
- __all__ = ['JiraPlus', 'ServerType', 'BASIC_FIELDS']
30
+ __all__ = ["JiraPlus", "ServerType", "BASIC_FIELDS"]
@@ -1,42 +1,43 @@
1
1
  # describe_allowed_value.py
2
2
 
3
3
  from functools import singledispatch
4
+ from typing import Any
4
5
 
5
6
 
6
7
  @singledispatch
7
- def describe_allowed_value(value, allowed_values: set = None):
8
+ def describe_allowed_value(value: Any, allowed_values: set = None) -> bool:
8
9
  raise NotImplementedError(f"cannot describe this object of type {type(value)}")
9
10
 
10
11
 
11
12
  @describe_allowed_value.register
12
- def _(value: int, allowed_values: list = None):
13
+ def _(value: int, allowed_values: list = None) -> bool:
13
14
  return value == allowed_values
14
15
 
15
16
 
16
17
  @describe_allowed_value.register
17
- def _(value: float, allowed_values: list = None):
18
+ def _(value: float, allowed_values: list = None) -> bool:
18
19
  return value == allowed_values
19
20
 
20
21
 
21
22
  @describe_allowed_value.register
22
- def _(value: str, allowed_values: list = None):
23
+ def _(value: str, allowed_values: list = None) -> bool:
23
24
  return value == allowed_values
24
25
 
25
26
 
26
27
  @describe_allowed_value.register
27
- def _(value: list, allowed_values: list = None):
28
- if all(isinstance(v, (str, int, float)) for v in value):
28
+ def _(value: list, allowed_values: list = None) -> bool:
29
+ if all(isinstance(v, str | int | float) for v in value):
29
30
  return any(v in allowed_values for v in value)
30
31
  if all(isinstance(v, dict) for v in value):
31
- return any([list(v.values())[0] in allowed_values for v in value])
32
+ return any(list(v.values())[0] in allowed_values for v in value)
32
33
  return False
33
34
 
34
35
 
35
36
  @describe_allowed_value.register
36
- def _(value: set, allowed_values: list = None):
37
+ def _(value: set, allowed_values: list = None) -> bool:
37
38
  return any(v in allowed_values for v in value)
38
39
 
39
40
 
40
41
  @describe_allowed_value.register
41
- def _(value: dict, allowed_values: list = None):
42
+ def _(value: dict, allowed_values: list = None) -> bool:
42
43
  return any(v in allowed_values for v in value.values())
@@ -5,10 +5,11 @@ import logging
5
5
  import os
6
6
  import time
7
7
  from enum import Enum
8
- from typing import Any, Union, Optional
8
+ from typing import Any
9
+
9
10
  import requests
10
11
  from custom_python_logger import get_logger
11
- from jira import JIRA, JIRAError, Issue
12
+ from jira import JIRA, Issue, JIRAError
12
13
  from jira.client import ResultList
13
14
  from retrying import retry
14
15
 
@@ -16,36 +17,37 @@ from python_jira_plus.describe_allowed_value import describe_allowed_value
16
17
 
17
18
 
18
19
  class UrlScheme(Enum):
19
- HTTP = 'http'
20
- HTTPS = 'https'
20
+ HTTP = "http"
21
+ HTTPS = "https"
21
22
 
22
23
 
23
24
  class ServerType(Enum):
24
- CLOUD = 'cloud'
25
- ON_PREMISE = 'on_premise'
25
+ CLOUD = "cloud"
26
+ ON_PREMISE = "on_premise"
26
27
 
27
28
 
28
29
  class JiraPlus:
29
30
  def __init__(
30
- self, base_url: Optional[str] = None,
31
+ self,
32
+ base_url: str | None = None,
31
33
  server_type: ServerType = ServerType.CLOUD,
32
34
  urllib3_log_level: int = logging.WARNING,
33
- jira_username: Optional[str] = None,
34
- jira_token: Optional[str] = None, # Use API token (not PAT)
35
+ jira_username: str | None = None,
36
+ jira_token: str | None = None, # Use API token (not PAT)
35
37
  verify_ssl: bool = True,
36
38
  sso: bool = False,
37
- url_scheme: UrlScheme = UrlScheme.HTTPS
39
+ url_scheme: UrlScheme = UrlScheme.HTTPS,
38
40
  ) -> None:
39
41
  self.logger = get_logger(self.__class__.__name__)
40
42
  get_logger("urllib3").setLevel(urllib3_log_level)
41
43
 
42
- self.jira_username = jira_username or os.getenv('JIRA_USER_NAME')
43
- self.jira_token = jira_token or os.getenv('JIRA_TOKEN')
44
- self.base_url = base_url or os.getenv('JIRA_BASE_URL')
44
+ self.jira_username = jira_username or os.getenv("JIRA_USER_NAME")
45
+ self.jira_token = jira_token or os.getenv("JIRA_TOKEN")
46
+ self.base_url = base_url or os.getenv("JIRA_BASE_URL")
45
47
  self.url_scheme = url_scheme.value
46
48
 
47
49
  self.server_type = server_type
48
- self.server = f'{self.url_scheme}://{self.base_url}'
50
+ self.server = f"{self.url_scheme}://{self.base_url}"
49
51
  self.verify_ssl = verify_ssl
50
52
  self.sso = sso
51
53
 
@@ -53,26 +55,23 @@ class JiraPlus:
53
55
  self.check_client_connection()
54
56
 
55
57
  @retry(stop_max_attempt_number=3, wait_fixed=180000)
56
- def create_connection(self, timeout=580) -> Optional[JIRA]:
57
- if self.server_type == ServerType.ON_PREMISE and not self.sso:
58
+ def create_connection(self, timeout: int = 580) -> JIRA | None:
59
+ if self.server_type == ServerType.ON_PREMISE and not self.sso: # pylint: disable=W0160:
58
60
  jira_client = JIRA(
59
- token_auth=self.jira_token,
60
- options={'server': self.server, 'verify': self.verify_ssl},
61
- timeout=timeout
61
+ token_auth=self.jira_token, options={"server": self.server, "verify": self.verify_ssl}, timeout=timeout
62
62
  )
63
63
  else:
64
64
  jira_client = JIRA(
65
65
  basic_auth=(self.jira_username, self.jira_token),
66
- options={'server': self.server, 'verify': self.verify_ssl},
67
- timeout=timeout
66
+ options={"server": self.server, "verify": self.verify_ssl},
67
+ timeout=timeout,
68
68
  )
69
69
 
70
70
  if jira_client.projects():
71
- self.logger.info(f"Jira Connection Successful")
71
+ self.logger.info("Jira Connection Successful")
72
72
  return jira_client
73
- else:
74
- self.logger.error(f"Jira Connection Failed")
75
- raise JIRAError("Jira Connection Failed")
73
+ self.logger.error("Jira Connection Failed")
74
+ raise JIRAError("Jira Connection Failed")
76
75
 
77
76
  def close_connection(self) -> None:
78
77
  try:
@@ -88,9 +87,8 @@ class JiraPlus:
88
87
  try:
89
88
  if self.jira_client.projects():
90
89
  return True
91
- else:
92
- self.logger.info("Jira client is closed")
93
- return False
90
+ self.logger.info("Jira client is closed")
91
+ return False
94
92
  except JIRAError as err:
95
93
  self.logger.exception(f"JIRAError \n{err}")
96
94
  return False
@@ -99,10 +97,7 @@ class JiraPlus:
99
97
  return False
100
98
 
101
99
  def get_issue_by_key(
102
- self,
103
- key: str,
104
- specific_fields: str | list[str] = '*all',
105
- json_result: bool = True
100
+ self, key: str, specific_fields: str | list[str] = "*all", json_result: bool = True
106
101
  ) -> None | Issue | dict[str, Any]:
107
102
  try:
108
103
  jira_issue = self.jira_client.issue(id=key, fields=specific_fields)
@@ -120,11 +115,15 @@ class JiraPlus:
120
115
  self,
121
116
  query: str,
122
117
  max_results: int = 5000,
123
- specific_fields: Union[str, list[str]] = "*all",
118
+ specific_fields: str | list[str] = "*all",
124
119
  jira_err_count: int = 3,
125
120
  json_result: bool = True,
126
- ) -> Optional[Union[ResultList[Issue], dict]]:
127
- return self._paginate_query(
121
+ ) -> ResultList[Issue] | dict | None:
122
+ if self.server_type == ServerType.ON_PREMISE:
123
+ _paginate_query_func = self._paginate_query
124
+ else:
125
+ _paginate_query_func = self._paginate_query_new
126
+ return _paginate_query_func(
128
127
  query=query,
129
128
  max_results=max_results,
130
129
  specific_fields=specific_fields,
@@ -132,30 +131,67 @@ class JiraPlus:
132
131
  json_result=json_result,
133
132
  )
134
133
 
134
+ def _paginate_query_new(
135
+ self,
136
+ query: str,
137
+ max_results: int,
138
+ specific_fields: str | list[str],
139
+ jira_err_limit: int,
140
+ json_result: bool,
141
+ ) -> ResultList[Issue] | list:
142
+ all_issues = []
143
+ retries = 0
144
+ page = None
145
+ while True: # pylint: disable=W0149
146
+ try:
147
+ self.logger.debug(f"Fetching issues from startAt={len(all_issues)}")
148
+ page = self.jira_client.enhanced_search_issues(
149
+ jql_str=query,
150
+ nextPageToken=page['nextPageToken'] if page else None,
151
+ maxResults=max_results,
152
+ fields=specific_fields,
153
+ json_result=json_result,
154
+ )
155
+ all_issues += page["issues"] if json_result else page
156
+ if page['isLast']:
157
+ break
158
+ retries = 0 # reset on success
159
+ except JIRAError as err:
160
+ if err.status_code == 400:
161
+ self.logger.error(f"Bad Request: {err.text}")
162
+ raise JIRAError(f"Bad Request: {err.text}") from err
163
+ retries += 1
164
+ self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
165
+ if retries >= jira_err_limit:
166
+ self.logger.exception("Max retries exceeded.")
167
+ return []
168
+ time.sleep(300 if retries == jira_err_limit - 1 else 90)
169
+ return all_issues
170
+
135
171
  def _paginate_query(
136
172
  self,
137
173
  query: str,
138
174
  max_results: int,
139
- specific_fields: Union[str, list[str]],
175
+ specific_fields: str | list[str],
140
176
  jira_err_limit: int,
141
- json_result: bool
142
- ) -> Union[ResultList[Issue] | list]:
177
+ json_result: bool,
178
+ ) -> ResultList[Issue] | list:
143
179
  start_at = 0
144
180
  all_issues = []
145
181
  retries = 0
146
- while True:
182
+ while True: # pylint: disable=W0149
147
183
  try:
148
184
  self.logger.debug(f"Fetching issues from startAt={start_at}")
149
- page = self.jira_client.search_issues(
185
+ page = self.jira_client.enhanced_search_issues(
150
186
  jql_str=query,
151
187
  startAt=start_at,
152
188
  maxResults=max_results,
153
189
  fields=specific_fields,
154
- json_result=json_result
190
+ json_result=json_result,
155
191
  )
156
- all_issues += page['issues'] if json_result else page
192
+ all_issues += page["issues"] if json_result else page
157
193
  if json_result:
158
- start_at += page['maxResults']
194
+ start_at += page["maxResults"]
159
195
  else:
160
196
  start_at += page.maxResults
161
197
  if start_at >= max_results:
@@ -164,7 +200,7 @@ class JiraPlus:
164
200
  except JIRAError as err:
165
201
  if err.status_code == 400:
166
202
  self.logger.error(f"Bad Request: {err.text}")
167
- raise JIRAError(f"Bad Request: {err.text}")
203
+ raise JIRAError(f"Bad Request: {err.text}") from err
168
204
  retries += 1
169
205
  self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
170
206
  if retries >= jira_err_limit:
@@ -175,19 +211,13 @@ class JiraPlus:
175
211
 
176
212
  def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
177
213
  meta = self.jira_client.createmeta(
178
- projectKeys=project_key,
179
- issuetypeNames=issue_type,
180
- expand="projects.issuetypes.fields"
214
+ projectKeys=project_key, issuetypeNames=issue_type, expand="projects.issuetypes.fields"
181
215
  )
182
216
 
183
- project_meta = next((p for p in meta['projects'] if p['key'] == project_key), None)
184
- if not project_meta:
217
+ if not (project_meta := next((p for p in meta["projects"] if p["key"] == project_key), None)):
185
218
  raise ValueError(f"Project {project_key} not found in metadata")
186
219
 
187
- issue_meta = next(
188
- (it for it in project_meta['issuetypes'] if it['name'].lower() == issue_type.lower()),
189
- None
190
- )
220
+ issue_meta = next((it for it in project_meta["issuetypes"] if it["name"].lower() == issue_type.lower()), None)
191
221
  if not issue_meta:
192
222
  raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
193
223
  return meta, issue_meta
@@ -197,47 +227,44 @@ class JiraPlus:
197
227
  project_key: str,
198
228
  issue_type: str,
199
229
  field_id_or_name: str,
200
- ) -> Optional[list]:
230
+ ) -> list | None:
201
231
  """
202
232
  Returns the allowed values for a specific field in a project + issue type context.
203
233
  """
204
- fields_metadata = self.get_project_fields_metadata(
205
- project_key=project_key,
206
- issue_type=issue_type
207
- )
234
+ fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
208
235
 
209
236
  # Try to find by ID or fallback to case-insensitive name
210
- field = fields_metadata.get(field_id_or_name)
211
- if not field:
237
+ if not (field := fields_metadata.get(field_id_or_name)):
212
238
  # Try matching by name (case-insensitive)
213
- for fid, fmeta in fields_metadata.items():
214
- if fmeta['name'].lower() == field_id_or_name.lower():
239
+ for _, fmeta in fields_metadata.items():
240
+ if fmeta["name"].lower() == field_id_or_name.lower():
215
241
  field = fmeta
216
242
  break
217
243
 
218
244
  if not field:
219
245
  raise ValueError(
220
- f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'")
246
+ f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'"
247
+ )
221
248
 
222
- allowed = field.get('allowedValues')
223
- if not allowed:
249
+ if not (allowed := field.get("allowedValues")):
224
250
  self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
225
251
  return None
226
252
 
227
253
  return allowed
228
254
 
229
- def _is_value_allowed(self, value: Any, allowed_values: list[Any]) -> bool:
230
- allowed_names = {av.get('name') for av in allowed_values}
231
- allowed_keys = {av.get('key') for av in allowed_values}
255
+ @staticmethod
256
+ def _is_value_allowed(value: Any, allowed_values: list[Any]) -> bool:
257
+ allowed_names = {av.get("name") for av in allowed_values}
258
+ allowed_keys = {av.get("key") for av in allowed_values}
232
259
  allowed_values = allowed_names | allowed_keys
233
260
  return describe_allowed_value(value, allowed_values=allowed_values)
234
261
 
235
- def get_server_version(self):
236
- return tuple(self.jira_client.server_info()['versionNumbers'])
262
+ def get_server_version(self) -> tuple[int, ...]:
263
+ return tuple(self.jira_client.server_info()["versionNumbers"])
237
264
 
238
265
  def check_server_createmeta_compatibility(self) -> bool:
239
266
  server_version = self.get_server_version()
240
- if self.server_type == ServerType.ON_PREMISE and ''.join(str(x) for x in server_version) > '9.0.0':
267
+ if self.server_type == ServerType.ON_PREMISE and "".join(str(x) for x in server_version) > "9.0.0":
241
268
  self.logger.warning(
242
269
  f"JIRA server version {server_version} is below the minimum required version (9.0.0). "
243
270
  "Some features may not work as expected."
@@ -247,49 +274,43 @@ class JiraPlus:
247
274
 
248
275
  def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
249
276
  if self.check_server_createmeta_compatibility():
250
- meta, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
251
- return issue_meta['fields']
252
- else:
253
- _project_issue_types = self.jira_client.project_issue_types(project_key)
254
- _issue_types = {t.name: t for t in _project_issue_types}
255
- field_list = self.jira_client.project_issue_fields(project_key, _issue_types[issue_type].id)
256
- return {f.fieldId: f.raw for f in field_list}
257
-
258
- def validate_fields(
259
- self,
260
- project_key: str,
261
- issue_type: str,
262
- fields: Optional[dict] = None
277
+ _, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
278
+ return issue_meta["fields"]
279
+ _project_issue_types = self.jira_client.project_issue_types(project_key)
280
+ _issue_types = {t.name: t for t in _project_issue_types}
281
+ field_list = self.jira_client.project_issue_fields(project_key, _issue_types[issue_type].id)
282
+ return {f.fieldId: f.raw for f in field_list}
283
+
284
+ def validate_fields( # pylint: disable=R1260
285
+ self, project_key: str, issue_type: str, fields: dict | None = None
263
286
  ) -> None:
264
- fields_metadata = self.get_project_fields_metadata(
265
- project_key=project_key,
266
- issue_type=issue_type
267
- )
287
+ fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
268
288
  for custom_field, value in fields.items():
269
289
  if custom_field not in fields_metadata:
270
290
  raise ValueError(
271
291
  f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
272
292
  )
273
293
 
274
- if allowed_value := fields_metadata[custom_field].get('allowedValues'):
294
+ if allowed_value := fields_metadata[custom_field].get("allowedValues"):
275
295
  if not self._is_value_allowed(value=value, allowed_values=allowed_value):
276
296
  raise ValueError(
277
- f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}' with issue type '{issue_type}'."
297
+ f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}' "
298
+ f"with issue type '{issue_type}'."
278
299
  f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
279
300
  )
280
301
 
281
- field_type = fields_metadata[custom_field]['schema']['type']
282
- if field_type == 'string' and not isinstance(value, str):
302
+ field_type = fields_metadata[custom_field]["schema"]["type"]
303
+ if field_type == "string" and not isinstance(value, str):
283
304
  raise ValueError(f"Field '{custom_field}' must be a string")
284
- elif field_type == 'number' and not isinstance(value, (int, float)):
305
+ if field_type == "number" and not isinstance(value, int | float):
285
306
  raise ValueError(f"Field '{custom_field}' must be a number")
286
- elif field_type == 'boolean' and not isinstance(value, bool):
307
+ if field_type == "boolean" and not isinstance(value, bool):
287
308
  raise ValueError(f"Field '{custom_field}' must be a boolean")
288
- elif field_type == 'array' and not isinstance(value, list):
309
+ if field_type == "array" and not isinstance(value, list):
289
310
  raise ValueError(f"Field '{custom_field}' must be an array")
290
- elif field_type == 'object' and not isinstance(value, dict):
311
+ if field_type == "object" and not isinstance(value, dict):
291
312
  raise ValueError(f"Field '{custom_field}' must be an object")
292
- elif field_type == 'issuetype' and not isinstance(value, dict):
313
+ if field_type == "issuetype" and not isinstance(value, dict):
293
314
  raise ValueError(f"Field '{custom_field}' must be an issue type")
294
315
  # Add more field type checks as needed
295
316
 
@@ -299,9 +320,9 @@ class JiraPlus:
299
320
  summary: str,
300
321
  description: str,
301
322
  issue_type: str,
302
- assignee: Optional[str] = None,
303
- custom_fields: Optional[dict] = None,
304
- ) -> Optional[Issue]:
323
+ assignee: str | None = None,
324
+ custom_fields: dict | None = None,
325
+ ) -> Issue | None:
305
326
  issue_fields = {
306
327
  "project": {"key": project_key},
307
328
  "issuetype": {"name": issue_type},
@@ -309,8 +330,9 @@ class JiraPlus:
309
330
  "description": description,
310
331
  }
311
332
  if assignee:
312
- issue_fields["assignee"] = {"accountId": assignee} if self.server_type == ServerType.CLOUD else {
313
- "name": assignee}
333
+ issue_fields["assignee"] = (
334
+ {"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
335
+ )
314
336
 
315
337
  if custom_fields:
316
338
  issue_fields = issue_fields | custom_fields
@@ -320,17 +342,11 @@ class JiraPlus:
320
342
  self.logger.info(f"Issue {issue.key} created successfully.")
321
343
  return issue
322
344
 
323
- def update_issue(
324
- self,
325
- issue_key: str,
326
- fields_to_update: dict
327
- ) -> Optional[Issue]:
345
+ def update_issue(self, issue_key: str, fields_to_update: dict) -> Issue | None:
328
346
  try:
329
347
  issue = self.jira_client.issue(issue_key)
330
348
  self.validate_fields(
331
- project_key=issue.fields.project.key,
332
- issue_type=issue.fields.issuetype.name,
333
- fields=fields_to_update
349
+ project_key=issue.fields.project.key, issue_type=issue.fields.issuetype.name, fields=fields_to_update
334
350
  )
335
351
 
336
352
  issue.update(fields=fields_to_update)
@@ -399,12 +415,12 @@ class JiraPlus:
399
415
  self,
400
416
  issue_key: str,
401
417
  transition_name: str,
402
- fields: Optional[dict] = None,
403
- comment: Optional[str] = None,
418
+ fields: dict | None = None,
419
+ comment: str | None = None,
404
420
  ) -> bool:
405
421
  try:
406
422
  transitions = self.jira_client.transitions(issue_key)
407
- transition_names = {t['name'] for t in transitions}
423
+ transition_names = {t["name"] for t in transitions}
408
424
  if transition_name not in transition_names:
409
425
  self.logger.error(
410
426
  f"Transition '{transition_name}' not found for issue {issue_key}. "
@@ -412,19 +428,13 @@ class JiraPlus:
412
428
  )
413
429
  return False
414
430
 
415
- transition_id = next(
416
- (t['id'] for t in transitions if t['name'].lower() == transition_name.lower()),
417
- None
418
- )
431
+ transition_id = next((t["id"] for t in transitions if t["name"].lower() == transition_name.lower()), None)
419
432
  if not transition_id:
420
433
  self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
421
434
  return False
422
435
 
423
436
  self.jira_client.transition_issue(
424
- issue=issue_key,
425
- transition=transition_id,
426
- fields=fields or {},
427
- comment=comment or None
437
+ issue=issue_key, transition=transition_id, fields=fields or {}, comment=comment or None
428
438
  )
429
439
  self.logger.info(f"Issue {issue_key} transitioned to '{transition_name}'")
430
440
  return True
@@ -436,15 +446,16 @@ class JiraPlus:
436
446
  return False
437
447
 
438
448
  def get_account_data(self, user_mail: str) -> list[dict]:
439
- jira_rst_url = \
440
- f'https://{self.jira_username}:{self.jira_token}@{self.base_url}/rest/api/{3}/user/search?query={user_mail}'
449
+ jira_rst_url = (
450
+ f"https://{self.jira_username}:{self.jira_token}@{self.base_url}/rest/api/{3}/user/search?query={user_mail}"
451
+ )
441
452
  response = requests.get(jira_rst_url)
442
453
  return response.json()
443
454
 
444
455
  def get_account_id(self, user_mail: str) -> str:
445
456
  data = self.get_account_data(user_mail=user_mail)
446
- return data[0]['accountId']
457
+ return data[0]["accountId"]
447
458
 
448
459
  def get_account_display_name(self, user_display_name: str) -> str:
449
460
  data = self.get_account_data(user_mail=user_display_name)
450
- return data[0]['displayName']
461
+ return data[0]["displayName"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jira-plus
3
- Version: 1.0.14
3
+ Version: 2.0.1
4
4
  Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation.
5
5
  Home-page: https://github.com/aviz92/python-jira-plus
6
6
  Author: Avi Zaguri
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: setuptools
13
13
  Requires-Dist: wheel
14
14
  Requires-Dist: dotenv
15
+ Requires-Dist: pre-commit
15
16
  Requires-Dist: custom-python-logger
16
17
  Requires-Dist: pytest
17
18
  Requires-Dist: requests
@@ -1,6 +1,7 @@
1
1
  setuptools
2
2
  wheel
3
3
  dotenv
4
+ pre-commit
4
5
  custom-python-logger
5
6
  pytest
6
7
  requests
@@ -1,6 +1,7 @@
1
1
  setuptools
2
2
  wheel
3
3
  dotenv
4
+ pre-commit
4
5
  custom-python-logger
5
6
  pytest
6
7
  requests
@@ -0,0 +1,35 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ package_version = "2.0.1"
4
+
5
+ package_name = "python-jira-plus"
6
+ package_description = "Enhanced Python client for JIRA with better error handling, pagination, and metadata validation."
7
+
8
+ package_name_ = package_name.replace("-", "_")
9
+ package_long_description_content_type = "text/markdown"
10
+ package_url = f"https://github.com/aviz92/{package_name}"
11
+ package_python_requires = ">=3.11"
12
+ package_author = "Avi Zaguri"
13
+
14
+ with open("requirements.txt") as file:
15
+ package_install_requires = [line.strip() for line in file.readlines() if line.strip() and not line.startswith("#")]
16
+
17
+ with open("README.md") as file:
18
+ package_long_description = file.read()
19
+
20
+ setup(
21
+ name=package_name,
22
+ version=package_version,
23
+ packages=find_packages(include=[package_name_, f"{package_name_}.*"]),
24
+ install_requires=package_install_requires,
25
+ author=package_author,
26
+ author_email="",
27
+ description=package_description,
28
+ long_description=package_long_description,
29
+ long_description_content_type=package_long_description_content_type,
30
+ url=package_url,
31
+ project_urls={
32
+ "Repository": package_url,
33
+ },
34
+ python_requires=package_python_requires,
35
+ )
@@ -1,37 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- package_version = '1.0.14'
4
-
5
- package_name = 'python-jira-plus'
6
- package_description = 'Enhanced Python client for JIRA with better error handling, pagination, and metadata validation.'
7
-
8
- package_name_ = package_name.replace('-', '_')
9
- package_long_description_content_type = 'text/markdown'
10
- package_url = f'https://github.com/aviz92/{package_name}'
11
- package_python_requires = '>=3.11'
12
- package_author = 'Avi Zaguri'
13
-
14
- with open('requirements.txt', 'r') as file:
15
- package_install_requires = [
16
- line.strip() for line in file.readlines() if line.strip() and not line.startswith('#')
17
- ]
18
-
19
- with open('README.md', 'r') as file:
20
- package_long_description = file.read()
21
-
22
- setup(
23
- name=package_name,
24
- version=package_version,
25
- packages=find_packages(include=[package_name_, f'{package_name_}.*']),
26
- install_requires=package_install_requires,
27
- author=package_author,
28
- author_email='',
29
- description=package_description,
30
- long_description=package_long_description,
31
- long_description_content_type=package_long_description_content_type,
32
- url=package_url,
33
- project_urls={
34
- 'Repository': package_url,
35
- },
36
- python_requires=package_python_requires,
37
- )