python-jira-plus 1.0.13__tar.gz → 1.0.15__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.13/python_jira_plus.egg-info → python_jira_plus-1.0.15}/PKG-INFO +2 -1
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus/__init__.py +15 -18
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus/describe_allowed_value.py +10 -9
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus/jira_plus.py +121 -150
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15/python_jira_plus.egg-info}/PKG-INFO +2 -1
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/requires.txt +1 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/requirements.txt +1 -0
- python_jira_plus-1.0.15/setup.py +35 -0
- python_jira_plus-1.0.13/setup.py +0 -37
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/LICENSE +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/MANIFEST.in +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/README.md +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/pyproject.toml +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/SOURCES.txt +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/dependency_links.txt +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/top_level.txt +0 -0
- {python_jira_plus-1.0.13 → python_jira_plus-1.0.15}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.15
|
|
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.13 → python_jira_plus-1.0.15}/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,9 +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
|
-
from
|
|
11
|
+
from custom_python_logger import get_logger
|
|
12
|
+
from jira import JIRA, Issue, JIRAError
|
|
11
13
|
from jira.client import ResultList
|
|
12
14
|
from retrying import retry
|
|
13
15
|
|
|
@@ -15,36 +17,37 @@ from python_jira_plus.describe_allowed_value import describe_allowed_value
|
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class UrlScheme(Enum):
|
|
18
|
-
HTTP =
|
|
19
|
-
HTTPS =
|
|
20
|
+
HTTP = "http"
|
|
21
|
+
HTTPS = "https"
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class ServerType(Enum):
|
|
23
|
-
CLOUD =
|
|
24
|
-
ON_PREMISE =
|
|
25
|
+
CLOUD = "cloud"
|
|
26
|
+
ON_PREMISE = "on_premise"
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class JiraPlus:
|
|
28
30
|
def __init__(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
self,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
server_type: ServerType = ServerType.CLOUD,
|
|
34
|
+
urllib3_log_level: int = logging.WARNING,
|
|
35
|
+
jira_username: str | None = None,
|
|
36
|
+
jira_token: str | None = None, # Use API token (not PAT)
|
|
37
|
+
verify_ssl: bool = True,
|
|
38
|
+
sso: bool = False,
|
|
39
|
+
url_scheme: UrlScheme = UrlScheme.HTTPS,
|
|
37
40
|
) -> None:
|
|
38
|
-
self.logger =
|
|
39
|
-
|
|
41
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
42
|
+
get_logger("urllib3").setLevel(urllib3_log_level)
|
|
40
43
|
|
|
41
|
-
self.jira_username = jira_username or os.getenv(
|
|
42
|
-
self.jira_token = jira_token or os.getenv(
|
|
43
|
-
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")
|
|
44
47
|
self.url_scheme = url_scheme.value
|
|
45
48
|
|
|
46
49
|
self.server_type = server_type
|
|
47
|
-
self.server = f
|
|
50
|
+
self.server = f"{self.url_scheme}://{self.base_url}"
|
|
48
51
|
self.verify_ssl = verify_ssl
|
|
49
52
|
self.sso = sso
|
|
50
53
|
|
|
@@ -52,26 +55,23 @@ class JiraPlus:
|
|
|
52
55
|
self.check_client_connection()
|
|
53
56
|
|
|
54
57
|
@retry(stop_max_attempt_number=3, wait_fixed=180000)
|
|
55
|
-
def create_connection(self, timeout=580) ->
|
|
56
|
-
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:
|
|
57
60
|
jira_client = JIRA(
|
|
58
|
-
token_auth=self.jira_token,
|
|
59
|
-
options={'server': self.server, 'verify': self.verify_ssl},
|
|
60
|
-
timeout=timeout
|
|
61
|
+
token_auth=self.jira_token, options={"server": self.server, "verify": self.verify_ssl}, timeout=timeout
|
|
61
62
|
)
|
|
62
63
|
else:
|
|
63
64
|
jira_client = JIRA(
|
|
64
65
|
basic_auth=(self.jira_username, self.jira_token),
|
|
65
|
-
options={
|
|
66
|
-
timeout=timeout
|
|
66
|
+
options={"server": self.server, "verify": self.verify_ssl},
|
|
67
|
+
timeout=timeout,
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
if jira_client.projects():
|
|
70
|
-
self.logger.info(
|
|
71
|
+
self.logger.info("Jira Connection Successful")
|
|
71
72
|
return jira_client
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
raise JIRAError("Jira Connection Failed")
|
|
73
|
+
self.logger.error("Jira Connection Failed")
|
|
74
|
+
raise JIRAError("Jira Connection Failed")
|
|
75
75
|
|
|
76
76
|
def close_connection(self) -> None:
|
|
77
77
|
try:
|
|
@@ -87,9 +87,8 @@ class JiraPlus:
|
|
|
87
87
|
try:
|
|
88
88
|
if self.jira_client.projects():
|
|
89
89
|
return True
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return False
|
|
90
|
+
self.logger.info("Jira client is closed")
|
|
91
|
+
return False
|
|
93
92
|
except JIRAError as err:
|
|
94
93
|
self.logger.exception(f"JIRAError \n{err}")
|
|
95
94
|
return False
|
|
@@ -98,10 +97,7 @@ class JiraPlus:
|
|
|
98
97
|
return False
|
|
99
98
|
|
|
100
99
|
def get_issue_by_key(
|
|
101
|
-
|
|
102
|
-
key: str,
|
|
103
|
-
specific_fields: str | list[str] = '*all',
|
|
104
|
-
json_result: bool = True
|
|
100
|
+
self, key: str, specific_fields: str | list[str] = "*all", json_result: bool = True
|
|
105
101
|
) -> None | Issue | dict[str, Any]:
|
|
106
102
|
try:
|
|
107
103
|
jira_issue = self.jira_client.issue(id=key, fields=specific_fields)
|
|
@@ -116,13 +112,13 @@ class JiraPlus:
|
|
|
116
112
|
return None
|
|
117
113
|
|
|
118
114
|
def get_objects_by_query(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
) ->
|
|
115
|
+
self,
|
|
116
|
+
query: str,
|
|
117
|
+
max_results: int = 5000,
|
|
118
|
+
specific_fields: str | list[str] = "*all",
|
|
119
|
+
jira_err_count: int = 3,
|
|
120
|
+
json_result: bool = True,
|
|
121
|
+
) -> ResultList[Issue] | dict | None:
|
|
126
122
|
return self._paginate_query(
|
|
127
123
|
query=query,
|
|
128
124
|
max_results=max_results,
|
|
@@ -132,17 +128,17 @@ class JiraPlus:
|
|
|
132
128
|
)
|
|
133
129
|
|
|
134
130
|
def _paginate_query(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
) ->
|
|
131
|
+
self,
|
|
132
|
+
query: str,
|
|
133
|
+
max_results: int,
|
|
134
|
+
specific_fields: str | list[str],
|
|
135
|
+
jira_err_limit: int,
|
|
136
|
+
json_result: bool,
|
|
137
|
+
) -> ResultList[Issue] | list:
|
|
142
138
|
start_at = 0
|
|
143
139
|
all_issues = []
|
|
144
140
|
retries = 0
|
|
145
|
-
while True:
|
|
141
|
+
while True: # pylint: disable=W0149
|
|
146
142
|
try:
|
|
147
143
|
self.logger.debug(f"Fetching issues from startAt={start_at}")
|
|
148
144
|
page = self.jira_client.search_issues(
|
|
@@ -150,11 +146,11 @@ class JiraPlus:
|
|
|
150
146
|
startAt=start_at,
|
|
151
147
|
maxResults=max_results,
|
|
152
148
|
fields=specific_fields,
|
|
153
|
-
json_result=json_result
|
|
149
|
+
json_result=json_result,
|
|
154
150
|
)
|
|
155
|
-
all_issues += page[
|
|
151
|
+
all_issues += page["issues"] if json_result else page
|
|
156
152
|
if json_result:
|
|
157
|
-
start_at += page[
|
|
153
|
+
start_at += page["maxResults"]
|
|
158
154
|
else:
|
|
159
155
|
start_at += page.maxResults
|
|
160
156
|
if start_at >= max_results:
|
|
@@ -163,7 +159,7 @@ class JiraPlus:
|
|
|
163
159
|
except JIRAError as err:
|
|
164
160
|
if err.status_code == 400:
|
|
165
161
|
self.logger.error(f"Bad Request: {err.text}")
|
|
166
|
-
raise JIRAError(f"Bad Request: {err.text}")
|
|
162
|
+
raise JIRAError(f"Bad Request: {err.text}") from err
|
|
167
163
|
retries += 1
|
|
168
164
|
self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
|
|
169
165
|
if retries >= jira_err_limit:
|
|
@@ -174,69 +170,60 @@ class JiraPlus:
|
|
|
174
170
|
|
|
175
171
|
def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
|
|
176
172
|
meta = self.jira_client.createmeta(
|
|
177
|
-
projectKeys=project_key,
|
|
178
|
-
issuetypeNames=issue_type,
|
|
179
|
-
expand="projects.issuetypes.fields"
|
|
173
|
+
projectKeys=project_key, issuetypeNames=issue_type, expand="projects.issuetypes.fields"
|
|
180
174
|
)
|
|
181
175
|
|
|
182
|
-
project_meta
|
|
183
|
-
if not project_meta:
|
|
176
|
+
if not (project_meta := next((p for p in meta["projects"] if p["key"] == project_key), None)):
|
|
184
177
|
raise ValueError(f"Project {project_key} not found in metadata")
|
|
185
178
|
|
|
186
|
-
issue_meta = next(
|
|
187
|
-
(it for it in project_meta['issuetypes'] if it['name'].lower() == issue_type.lower()),
|
|
188
|
-
None
|
|
189
|
-
)
|
|
179
|
+
issue_meta = next((it for it in project_meta["issuetypes"] if it["name"].lower() == issue_type.lower()), None)
|
|
190
180
|
if not issue_meta:
|
|
191
181
|
raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
|
|
192
182
|
return meta, issue_meta
|
|
193
183
|
|
|
194
184
|
def get_allowed_values(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
) ->
|
|
185
|
+
self,
|
|
186
|
+
project_key: str,
|
|
187
|
+
issue_type: str,
|
|
188
|
+
field_id_or_name: str,
|
|
189
|
+
) -> list | None:
|
|
200
190
|
"""
|
|
201
191
|
Returns the allowed values for a specific field in a project + issue type context.
|
|
202
192
|
"""
|
|
203
|
-
fields_metadata = self.get_project_fields_metadata(
|
|
204
|
-
project_key=project_key,
|
|
205
|
-
issue_type=issue_type
|
|
206
|
-
)
|
|
193
|
+
fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
|
|
207
194
|
|
|
208
195
|
# Try to find by ID or fallback to case-insensitive name
|
|
209
|
-
field
|
|
210
|
-
if not field:
|
|
196
|
+
if not (field := fields_metadata.get(field_id_or_name)):
|
|
211
197
|
# Try matching by name (case-insensitive)
|
|
212
|
-
for
|
|
213
|
-
if fmeta[
|
|
198
|
+
for _, fmeta in fields_metadata.items():
|
|
199
|
+
if fmeta["name"].lower() == field_id_or_name.lower():
|
|
214
200
|
field = fmeta
|
|
215
201
|
break
|
|
216
202
|
|
|
217
203
|
if not field:
|
|
218
204
|
raise ValueError(
|
|
219
|
-
f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'"
|
|
205
|
+
f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'"
|
|
206
|
+
)
|
|
220
207
|
|
|
221
|
-
allowed
|
|
222
|
-
if not allowed:
|
|
208
|
+
if not (allowed := field.get("allowedValues")):
|
|
223
209
|
self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
|
|
224
210
|
return None
|
|
225
211
|
|
|
226
212
|
return allowed
|
|
227
213
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _is_value_allowed(value: Any, allowed_values: list[Any]) -> bool:
|
|
216
|
+
allowed_names = {av.get("name") for av in allowed_values}
|
|
217
|
+
allowed_keys = {av.get("key") for av in allowed_values}
|
|
231
218
|
allowed_values = allowed_names | allowed_keys
|
|
232
219
|
return describe_allowed_value(value, allowed_values=allowed_values)
|
|
233
220
|
|
|
234
|
-
def get_server_version(self):
|
|
235
|
-
return tuple(self.jira_client.server_info()[
|
|
221
|
+
def get_server_version(self) -> tuple[int, ...]:
|
|
222
|
+
return tuple(self.jira_client.server_info()["versionNumbers"])
|
|
236
223
|
|
|
237
224
|
def check_server_createmeta_compatibility(self) -> bool:
|
|
238
225
|
server_version = self.get_server_version()
|
|
239
|
-
if self.server_type == ServerType.ON_PREMISE and
|
|
226
|
+
if self.server_type == ServerType.ON_PREMISE and "".join(str(x) for x in server_version) > "9.0.0":
|
|
240
227
|
self.logger.warning(
|
|
241
228
|
f"JIRA server version {server_version} is below the minimum required version (9.0.0). "
|
|
242
229
|
"Some features may not work as expected."
|
|
@@ -246,61 +233,55 @@ class JiraPlus:
|
|
|
246
233
|
|
|
247
234
|
def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
|
|
248
235
|
if self.check_server_createmeta_compatibility():
|
|
249
|
-
|
|
250
|
-
return issue_meta[
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
self,
|
|
259
|
-
project_key: str,
|
|
260
|
-
issue_type: str,
|
|
261
|
-
fields: Optional[dict] = None
|
|
236
|
+
_, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
|
|
237
|
+
return issue_meta["fields"]
|
|
238
|
+
_project_issue_types = self.jira_client.project_issue_types(project_key)
|
|
239
|
+
_issue_types = {t.name: t for t in _project_issue_types}
|
|
240
|
+
field_list = self.jira_client.project_issue_fields(project_key, _issue_types[issue_type].id)
|
|
241
|
+
return {f.fieldId: f.raw for f in field_list}
|
|
242
|
+
|
|
243
|
+
def validate_fields( # pylint: disable=R1260
|
|
244
|
+
self, project_key: str, issue_type: str, fields: dict | None = None
|
|
262
245
|
) -> None:
|
|
263
|
-
fields_metadata = self.get_project_fields_metadata(
|
|
264
|
-
project_key=project_key,
|
|
265
|
-
issue_type=issue_type
|
|
266
|
-
)
|
|
246
|
+
fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
|
|
267
247
|
for custom_field, value in fields.items():
|
|
268
248
|
if custom_field not in fields_metadata:
|
|
269
249
|
raise ValueError(
|
|
270
250
|
f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
|
|
271
251
|
)
|
|
272
252
|
|
|
273
|
-
if allowed_value := fields_metadata[custom_field].get(
|
|
253
|
+
if allowed_value := fields_metadata[custom_field].get("allowedValues"):
|
|
274
254
|
if not self._is_value_allowed(value=value, allowed_values=allowed_value):
|
|
275
255
|
raise ValueError(
|
|
276
|
-
f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}'
|
|
256
|
+
f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}' "
|
|
257
|
+
f"with issue type '{issue_type}'."
|
|
277
258
|
f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
|
|
278
259
|
)
|
|
279
260
|
|
|
280
|
-
field_type = fields_metadata[custom_field][
|
|
281
|
-
if field_type ==
|
|
261
|
+
field_type = fields_metadata[custom_field]["schema"]["type"]
|
|
262
|
+
if field_type == "string" and not isinstance(value, str):
|
|
282
263
|
raise ValueError(f"Field '{custom_field}' must be a string")
|
|
283
|
-
|
|
264
|
+
if field_type == "number" and not isinstance(value, int | float):
|
|
284
265
|
raise ValueError(f"Field '{custom_field}' must be a number")
|
|
285
|
-
|
|
266
|
+
if field_type == "boolean" and not isinstance(value, bool):
|
|
286
267
|
raise ValueError(f"Field '{custom_field}' must be a boolean")
|
|
287
|
-
|
|
268
|
+
if field_type == "array" and not isinstance(value, list):
|
|
288
269
|
raise ValueError(f"Field '{custom_field}' must be an array")
|
|
289
|
-
|
|
270
|
+
if field_type == "object" and not isinstance(value, dict):
|
|
290
271
|
raise ValueError(f"Field '{custom_field}' must be an object")
|
|
291
|
-
|
|
272
|
+
if field_type == "issuetype" and not isinstance(value, dict):
|
|
292
273
|
raise ValueError(f"Field '{custom_field}' must be an issue type")
|
|
293
274
|
# Add more field type checks as needed
|
|
294
275
|
|
|
295
276
|
def create_issue(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
) ->
|
|
277
|
+
self,
|
|
278
|
+
project_key: str,
|
|
279
|
+
summary: str,
|
|
280
|
+
description: str,
|
|
281
|
+
issue_type: str,
|
|
282
|
+
assignee: str | None = None,
|
|
283
|
+
custom_fields: dict | None = None,
|
|
284
|
+
) -> Issue | None:
|
|
304
285
|
issue_fields = {
|
|
305
286
|
"project": {"key": project_key},
|
|
306
287
|
"issuetype": {"name": issue_type},
|
|
@@ -308,8 +289,9 @@ class JiraPlus:
|
|
|
308
289
|
"description": description,
|
|
309
290
|
}
|
|
310
291
|
if assignee:
|
|
311
|
-
issue_fields["assignee"] =
|
|
312
|
-
"name": assignee}
|
|
292
|
+
issue_fields["assignee"] = (
|
|
293
|
+
{"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
|
|
294
|
+
)
|
|
313
295
|
|
|
314
296
|
if custom_fields:
|
|
315
297
|
issue_fields = issue_fields | custom_fields
|
|
@@ -319,17 +301,11 @@ class JiraPlus:
|
|
|
319
301
|
self.logger.info(f"Issue {issue.key} created successfully.")
|
|
320
302
|
return issue
|
|
321
303
|
|
|
322
|
-
def update_issue(
|
|
323
|
-
self,
|
|
324
|
-
issue_key: str,
|
|
325
|
-
fields_to_update: dict
|
|
326
|
-
) -> Optional[Issue]:
|
|
304
|
+
def update_issue(self, issue_key: str, fields_to_update: dict) -> Issue | None:
|
|
327
305
|
try:
|
|
328
306
|
issue = self.jira_client.issue(issue_key)
|
|
329
307
|
self.validate_fields(
|
|
330
|
-
project_key=issue.fields.project.key,
|
|
331
|
-
issue_type=issue.fields.issuetype.name,
|
|
332
|
-
fields=fields_to_update
|
|
308
|
+
project_key=issue.fields.project.key, issue_type=issue.fields.issuetype.name, fields=fields_to_update
|
|
333
309
|
)
|
|
334
310
|
|
|
335
311
|
issue.update(fields=fields_to_update)
|
|
@@ -395,15 +371,15 @@ class JiraPlus:
|
|
|
395
371
|
self.logger.exception(f"Exception - Attachment {file_path} was not uploaded to issue {issue_key}\n{err}")
|
|
396
372
|
|
|
397
373
|
def transition_issue(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
374
|
+
self,
|
|
375
|
+
issue_key: str,
|
|
376
|
+
transition_name: str,
|
|
377
|
+
fields: dict | None = None,
|
|
378
|
+
comment: str | None = None,
|
|
403
379
|
) -> bool:
|
|
404
380
|
try:
|
|
405
381
|
transitions = self.jira_client.transitions(issue_key)
|
|
406
|
-
transition_names = {t[
|
|
382
|
+
transition_names = {t["name"] for t in transitions}
|
|
407
383
|
if transition_name not in transition_names:
|
|
408
384
|
self.logger.error(
|
|
409
385
|
f"Transition '{transition_name}' not found for issue {issue_key}. "
|
|
@@ -411,19 +387,13 @@ class JiraPlus:
|
|
|
411
387
|
)
|
|
412
388
|
return False
|
|
413
389
|
|
|
414
|
-
transition_id = next(
|
|
415
|
-
(t['id'] for t in transitions if t['name'].lower() == transition_name.lower()),
|
|
416
|
-
None
|
|
417
|
-
)
|
|
390
|
+
transition_id = next((t["id"] for t in transitions if t["name"].lower() == transition_name.lower()), None)
|
|
418
391
|
if not transition_id:
|
|
419
392
|
self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
|
|
420
393
|
return False
|
|
421
394
|
|
|
422
395
|
self.jira_client.transition_issue(
|
|
423
|
-
issue=issue_key,
|
|
424
|
-
transition=transition_id,
|
|
425
|
-
fields=fields or {},
|
|
426
|
-
comment=comment or None
|
|
396
|
+
issue=issue_key, transition=transition_id, fields=fields or {}, comment=comment or None
|
|
427
397
|
)
|
|
428
398
|
self.logger.info(f"Issue {issue_key} transitioned to '{transition_name}'")
|
|
429
399
|
return True
|
|
@@ -435,15 +405,16 @@ class JiraPlus:
|
|
|
435
405
|
return False
|
|
436
406
|
|
|
437
407
|
def get_account_data(self, user_mail: str) -> list[dict]:
|
|
438
|
-
jira_rst_url =
|
|
439
|
-
f
|
|
408
|
+
jira_rst_url = (
|
|
409
|
+
f"https://{self.jira_username}:{self.jira_token}@{self.base_url}/rest/api/{3}/user/search?query={user_mail}"
|
|
410
|
+
)
|
|
440
411
|
response = requests.get(jira_rst_url)
|
|
441
412
|
return response.json()
|
|
442
413
|
|
|
443
414
|
def get_account_id(self, user_mail: str) -> str:
|
|
444
415
|
data = self.get_account_data(user_mail=user_mail)
|
|
445
|
-
return data[0][
|
|
416
|
+
return data[0]["accountId"]
|
|
446
417
|
|
|
447
418
|
def get_account_display_name(self, user_display_name: str) -> str:
|
|
448
419
|
data = self.get_account_data(user_mail=user_display_name)
|
|
449
|
-
return data[0][
|
|
420
|
+
return data[0]["displayName"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.15
|
|
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 = "1.0.15"
|
|
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.13/setup.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
package_version = '1.0.13'
|
|
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.13 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|