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.
- {python_jira_plus-1.0.14/python_jira_plus.egg-info → python_jira_plus-2.0.1}/PKG-INFO +2 -1
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus/__init__.py +15 -18
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus/describe_allowed_value.py +10 -9
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus/jira_plus.py +136 -125
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1/python_jira_plus.egg-info}/PKG-INFO +2 -1
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus.egg-info/requires.txt +1 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/requirements.txt +1 -0
- python_jira_plus-2.0.1/setup.py +35 -0
- python_jira_plus-1.0.14/setup.py +0 -37
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/LICENSE +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/MANIFEST.in +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/README.md +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/pyproject.toml +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus.egg-info/SOURCES.txt +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus.egg-info/dependency_links.txt +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus.egg-info/top_level.txt +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version:
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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__ = [
|
|
30
|
+
__all__ = ["JiraPlus", "ServerType", "BASIC_FIELDS"]
|
{python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus/describe_allowed_value.py
RENAMED
|
@@ -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,
|
|
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(
|
|
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
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
9
10
|
import requests
|
|
10
11
|
from custom_python_logger import get_logger
|
|
11
|
-
from jira import JIRA,
|
|
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 =
|
|
20
|
-
HTTPS =
|
|
20
|
+
HTTP = "http"
|
|
21
|
+
HTTPS = "https"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ServerType(Enum):
|
|
24
|
-
CLOUD =
|
|
25
|
-
ON_PREMISE =
|
|
25
|
+
CLOUD = "cloud"
|
|
26
|
+
ON_PREMISE = "on_premise"
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class JiraPlus:
|
|
29
30
|
def __init__(
|
|
30
|
-
self,
|
|
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:
|
|
34
|
-
jira_token:
|
|
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(
|
|
43
|
-
self.jira_token = jira_token or os.getenv(
|
|
44
|
-
self.base_url = base_url or os.getenv(
|
|
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
|
|
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) ->
|
|
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={
|
|
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(
|
|
71
|
+
self.logger.info("Jira Connection Successful")
|
|
72
72
|
return jira_client
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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:
|
|
118
|
+
specific_fields: str | list[str] = "*all",
|
|
124
119
|
jira_err_count: int = 3,
|
|
125
120
|
json_result: bool = True,
|
|
126
|
-
) ->
|
|
127
|
-
|
|
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:
|
|
175
|
+
specific_fields: str | list[str],
|
|
140
176
|
jira_err_limit: int,
|
|
141
|
-
json_result: bool
|
|
142
|
-
) ->
|
|
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.
|
|
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[
|
|
192
|
+
all_issues += page["issues"] if json_result else page
|
|
157
193
|
if json_result:
|
|
158
|
-
start_at += page[
|
|
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
|
|
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
|
-
) ->
|
|
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
|
|
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
|
|
214
|
-
if fmeta[
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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()[
|
|
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
|
|
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
|
-
|
|
251
|
-
return issue_meta[
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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}'
|
|
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][
|
|
282
|
-
if field_type ==
|
|
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
|
-
|
|
305
|
+
if field_type == "number" and not isinstance(value, int | float):
|
|
285
306
|
raise ValueError(f"Field '{custom_field}' must be a number")
|
|
286
|
-
|
|
307
|
+
if field_type == "boolean" and not isinstance(value, bool):
|
|
287
308
|
raise ValueError(f"Field '{custom_field}' must be a boolean")
|
|
288
|
-
|
|
309
|
+
if field_type == "array" and not isinstance(value, list):
|
|
289
310
|
raise ValueError(f"Field '{custom_field}' must be an array")
|
|
290
|
-
|
|
311
|
+
if field_type == "object" and not isinstance(value, dict):
|
|
291
312
|
raise ValueError(f"Field '{custom_field}' must be an object")
|
|
292
|
-
|
|
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:
|
|
303
|
-
custom_fields:
|
|
304
|
-
) ->
|
|
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"] =
|
|
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:
|
|
403
|
-
comment:
|
|
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[
|
|
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
|
|
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][
|
|
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][
|
|
461
|
+
return data[0]["displayName"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version:
|
|
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
|
|
@@ -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
|
+
)
|
python_jira_plus-1.0.14/setup.py
DELETED
|
@@ -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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_jira_plus-1.0.14 → python_jira_plus-2.0.1}/python_jira_plus.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|