python-jira-plus 1.0.14__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.14/python_jira_plus.egg-info → python_jira_plus-1.0.15}/PKG-INFO +2 -1
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus/__init__.py +15 -18
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus/describe_allowed_value.py +10 -9
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus/jira_plus.py +93 -123
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15/python_jira_plus.egg-info}/PKG-INFO +2 -1
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/requires.txt +1 -0
- {python_jira_plus-1.0.14 → 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.14/setup.py +0 -37
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/LICENSE +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/MANIFEST.in +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/README.md +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/pyproject.toml +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/SOURCES.txt +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/dependency_links.txt +0 -0
- {python_jira_plus-1.0.14 → python_jira_plus-1.0.15}/python_jira_plus.egg-info/top_level.txt +0 -0
- {python_jira_plus-1.0.14 → 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.14 → 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,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,10 +115,10 @@ 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
|
-
) ->
|
|
121
|
+
) -> ResultList[Issue] | dict | None:
|
|
127
122
|
return self._paginate_query(
|
|
128
123
|
query=query,
|
|
129
124
|
max_results=max_results,
|
|
@@ -136,14 +131,14 @@ class JiraPlus:
|
|
|
136
131
|
self,
|
|
137
132
|
query: str,
|
|
138
133
|
max_results: int,
|
|
139
|
-
specific_fields:
|
|
134
|
+
specific_fields: str | list[str],
|
|
140
135
|
jira_err_limit: int,
|
|
141
|
-
json_result: bool
|
|
142
|
-
) ->
|
|
136
|
+
json_result: bool,
|
|
137
|
+
) -> ResultList[Issue] | list:
|
|
143
138
|
start_at = 0
|
|
144
139
|
all_issues = []
|
|
145
140
|
retries = 0
|
|
146
|
-
while True:
|
|
141
|
+
while True: # pylint: disable=W0149
|
|
147
142
|
try:
|
|
148
143
|
self.logger.debug(f"Fetching issues from startAt={start_at}")
|
|
149
144
|
page = self.jira_client.search_issues(
|
|
@@ -151,11 +146,11 @@ class JiraPlus:
|
|
|
151
146
|
startAt=start_at,
|
|
152
147
|
maxResults=max_results,
|
|
153
148
|
fields=specific_fields,
|
|
154
|
-
json_result=json_result
|
|
149
|
+
json_result=json_result,
|
|
155
150
|
)
|
|
156
|
-
all_issues += page[
|
|
151
|
+
all_issues += page["issues"] if json_result else page
|
|
157
152
|
if json_result:
|
|
158
|
-
start_at += page[
|
|
153
|
+
start_at += page["maxResults"]
|
|
159
154
|
else:
|
|
160
155
|
start_at += page.maxResults
|
|
161
156
|
if start_at >= max_results:
|
|
@@ -164,7 +159,7 @@ class JiraPlus:
|
|
|
164
159
|
except JIRAError as err:
|
|
165
160
|
if err.status_code == 400:
|
|
166
161
|
self.logger.error(f"Bad Request: {err.text}")
|
|
167
|
-
raise JIRAError(f"Bad Request: {err.text}")
|
|
162
|
+
raise JIRAError(f"Bad Request: {err.text}") from err
|
|
168
163
|
retries += 1
|
|
169
164
|
self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
|
|
170
165
|
if retries >= jira_err_limit:
|
|
@@ -175,19 +170,13 @@ class JiraPlus:
|
|
|
175
170
|
|
|
176
171
|
def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
|
|
177
172
|
meta = self.jira_client.createmeta(
|
|
178
|
-
projectKeys=project_key,
|
|
179
|
-
issuetypeNames=issue_type,
|
|
180
|
-
expand="projects.issuetypes.fields"
|
|
173
|
+
projectKeys=project_key, issuetypeNames=issue_type, expand="projects.issuetypes.fields"
|
|
181
174
|
)
|
|
182
175
|
|
|
183
|
-
project_meta
|
|
184
|
-
if not project_meta:
|
|
176
|
+
if not (project_meta := next((p for p in meta["projects"] if p["key"] == project_key), None)):
|
|
185
177
|
raise ValueError(f"Project {project_key} not found in metadata")
|
|
186
178
|
|
|
187
|
-
issue_meta = next(
|
|
188
|
-
(it for it in project_meta['issuetypes'] if it['name'].lower() == issue_type.lower()),
|
|
189
|
-
None
|
|
190
|
-
)
|
|
179
|
+
issue_meta = next((it for it in project_meta["issuetypes"] if it["name"].lower() == issue_type.lower()), None)
|
|
191
180
|
if not issue_meta:
|
|
192
181
|
raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
|
|
193
182
|
return meta, issue_meta
|
|
@@ -197,47 +186,44 @@ class JiraPlus:
|
|
|
197
186
|
project_key: str,
|
|
198
187
|
issue_type: str,
|
|
199
188
|
field_id_or_name: str,
|
|
200
|
-
) ->
|
|
189
|
+
) -> list | None:
|
|
201
190
|
"""
|
|
202
191
|
Returns the allowed values for a specific field in a project + issue type context.
|
|
203
192
|
"""
|
|
204
|
-
fields_metadata = self.get_project_fields_metadata(
|
|
205
|
-
project_key=project_key,
|
|
206
|
-
issue_type=issue_type
|
|
207
|
-
)
|
|
193
|
+
fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
|
|
208
194
|
|
|
209
195
|
# Try to find by ID or fallback to case-insensitive name
|
|
210
|
-
field
|
|
211
|
-
if not field:
|
|
196
|
+
if not (field := fields_metadata.get(field_id_or_name)):
|
|
212
197
|
# Try matching by name (case-insensitive)
|
|
213
|
-
for
|
|
214
|
-
if fmeta[
|
|
198
|
+
for _, fmeta in fields_metadata.items():
|
|
199
|
+
if fmeta["name"].lower() == field_id_or_name.lower():
|
|
215
200
|
field = fmeta
|
|
216
201
|
break
|
|
217
202
|
|
|
218
203
|
if not field:
|
|
219
204
|
raise ValueError(
|
|
220
|
-
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
|
+
)
|
|
221
207
|
|
|
222
|
-
allowed
|
|
223
|
-
if not allowed:
|
|
208
|
+
if not (allowed := field.get("allowedValues")):
|
|
224
209
|
self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
|
|
225
210
|
return None
|
|
226
211
|
|
|
227
212
|
return allowed
|
|
228
213
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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}
|
|
232
218
|
allowed_values = allowed_names | allowed_keys
|
|
233
219
|
return describe_allowed_value(value, allowed_values=allowed_values)
|
|
234
220
|
|
|
235
|
-
def get_server_version(self):
|
|
236
|
-
return tuple(self.jira_client.server_info()[
|
|
221
|
+
def get_server_version(self) -> tuple[int, ...]:
|
|
222
|
+
return tuple(self.jira_client.server_info()["versionNumbers"])
|
|
237
223
|
|
|
238
224
|
def check_server_createmeta_compatibility(self) -> bool:
|
|
239
225
|
server_version = self.get_server_version()
|
|
240
|
-
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":
|
|
241
227
|
self.logger.warning(
|
|
242
228
|
f"JIRA server version {server_version} is below the minimum required version (9.0.0). "
|
|
243
229
|
"Some features may not work as expected."
|
|
@@ -247,49 +233,43 @@ class JiraPlus:
|
|
|
247
233
|
|
|
248
234
|
def get_project_fields_metadata(self, project_key: str, issue_type: str) -> dict:
|
|
249
235
|
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
|
|
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
|
|
263
245
|
) -> None:
|
|
264
|
-
fields_metadata = self.get_project_fields_metadata(
|
|
265
|
-
project_key=project_key,
|
|
266
|
-
issue_type=issue_type
|
|
267
|
-
)
|
|
246
|
+
fields_metadata = self.get_project_fields_metadata(project_key=project_key, issue_type=issue_type)
|
|
268
247
|
for custom_field, value in fields.items():
|
|
269
248
|
if custom_field not in fields_metadata:
|
|
270
249
|
raise ValueError(
|
|
271
250
|
f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
|
|
272
251
|
)
|
|
273
252
|
|
|
274
|
-
if allowed_value := fields_metadata[custom_field].get(
|
|
253
|
+
if allowed_value := fields_metadata[custom_field].get("allowedValues"):
|
|
275
254
|
if not self._is_value_allowed(value=value, allowed_values=allowed_value):
|
|
276
255
|
raise ValueError(
|
|
277
|
-
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}'."
|
|
278
258
|
f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
|
|
279
259
|
)
|
|
280
260
|
|
|
281
|
-
field_type = fields_metadata[custom_field][
|
|
282
|
-
if field_type ==
|
|
261
|
+
field_type = fields_metadata[custom_field]["schema"]["type"]
|
|
262
|
+
if field_type == "string" and not isinstance(value, str):
|
|
283
263
|
raise ValueError(f"Field '{custom_field}' must be a string")
|
|
284
|
-
|
|
264
|
+
if field_type == "number" and not isinstance(value, int | float):
|
|
285
265
|
raise ValueError(f"Field '{custom_field}' must be a number")
|
|
286
|
-
|
|
266
|
+
if field_type == "boolean" and not isinstance(value, bool):
|
|
287
267
|
raise ValueError(f"Field '{custom_field}' must be a boolean")
|
|
288
|
-
|
|
268
|
+
if field_type == "array" and not isinstance(value, list):
|
|
289
269
|
raise ValueError(f"Field '{custom_field}' must be an array")
|
|
290
|
-
|
|
270
|
+
if field_type == "object" and not isinstance(value, dict):
|
|
291
271
|
raise ValueError(f"Field '{custom_field}' must be an object")
|
|
292
|
-
|
|
272
|
+
if field_type == "issuetype" and not isinstance(value, dict):
|
|
293
273
|
raise ValueError(f"Field '{custom_field}' must be an issue type")
|
|
294
274
|
# Add more field type checks as needed
|
|
295
275
|
|
|
@@ -299,9 +279,9 @@ class JiraPlus:
|
|
|
299
279
|
summary: str,
|
|
300
280
|
description: str,
|
|
301
281
|
issue_type: str,
|
|
302
|
-
assignee:
|
|
303
|
-
custom_fields:
|
|
304
|
-
) ->
|
|
282
|
+
assignee: str | None = None,
|
|
283
|
+
custom_fields: dict | None = None,
|
|
284
|
+
) -> Issue | None:
|
|
305
285
|
issue_fields = {
|
|
306
286
|
"project": {"key": project_key},
|
|
307
287
|
"issuetype": {"name": issue_type},
|
|
@@ -309,8 +289,9 @@ class JiraPlus:
|
|
|
309
289
|
"description": description,
|
|
310
290
|
}
|
|
311
291
|
if assignee:
|
|
312
|
-
issue_fields["assignee"] =
|
|
313
|
-
"name": assignee}
|
|
292
|
+
issue_fields["assignee"] = (
|
|
293
|
+
{"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
|
|
294
|
+
)
|
|
314
295
|
|
|
315
296
|
if custom_fields:
|
|
316
297
|
issue_fields = issue_fields | custom_fields
|
|
@@ -320,17 +301,11 @@ class JiraPlus:
|
|
|
320
301
|
self.logger.info(f"Issue {issue.key} created successfully.")
|
|
321
302
|
return issue
|
|
322
303
|
|
|
323
|
-
def update_issue(
|
|
324
|
-
self,
|
|
325
|
-
issue_key: str,
|
|
326
|
-
fields_to_update: dict
|
|
327
|
-
) -> Optional[Issue]:
|
|
304
|
+
def update_issue(self, issue_key: str, fields_to_update: dict) -> Issue | None:
|
|
328
305
|
try:
|
|
329
306
|
issue = self.jira_client.issue(issue_key)
|
|
330
307
|
self.validate_fields(
|
|
331
|
-
project_key=issue.fields.project.key,
|
|
332
|
-
issue_type=issue.fields.issuetype.name,
|
|
333
|
-
fields=fields_to_update
|
|
308
|
+
project_key=issue.fields.project.key, issue_type=issue.fields.issuetype.name, fields=fields_to_update
|
|
334
309
|
)
|
|
335
310
|
|
|
336
311
|
issue.update(fields=fields_to_update)
|
|
@@ -399,12 +374,12 @@ class JiraPlus:
|
|
|
399
374
|
self,
|
|
400
375
|
issue_key: str,
|
|
401
376
|
transition_name: str,
|
|
402
|
-
fields:
|
|
403
|
-
comment:
|
|
377
|
+
fields: dict | None = None,
|
|
378
|
+
comment: str | None = None,
|
|
404
379
|
) -> bool:
|
|
405
380
|
try:
|
|
406
381
|
transitions = self.jira_client.transitions(issue_key)
|
|
407
|
-
transition_names = {t[
|
|
382
|
+
transition_names = {t["name"] for t in transitions}
|
|
408
383
|
if transition_name not in transition_names:
|
|
409
384
|
self.logger.error(
|
|
410
385
|
f"Transition '{transition_name}' not found for issue {issue_key}. "
|
|
@@ -412,19 +387,13 @@ class JiraPlus:
|
|
|
412
387
|
)
|
|
413
388
|
return False
|
|
414
389
|
|
|
415
|
-
transition_id = next(
|
|
416
|
-
(t['id'] for t in transitions if t['name'].lower() == transition_name.lower()),
|
|
417
|
-
None
|
|
418
|
-
)
|
|
390
|
+
transition_id = next((t["id"] for t in transitions if t["name"].lower() == transition_name.lower()), None)
|
|
419
391
|
if not transition_id:
|
|
420
392
|
self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
|
|
421
393
|
return False
|
|
422
394
|
|
|
423
395
|
self.jira_client.transition_issue(
|
|
424
|
-
issue=issue_key,
|
|
425
|
-
transition=transition_id,
|
|
426
|
-
fields=fields or {},
|
|
427
|
-
comment=comment or None
|
|
396
|
+
issue=issue_key, transition=transition_id, fields=fields or {}, comment=comment or None
|
|
428
397
|
)
|
|
429
398
|
self.logger.info(f"Issue {issue_key} transitioned to '{transition_name}'")
|
|
430
399
|
return True
|
|
@@ -436,15 +405,16 @@ class JiraPlus:
|
|
|
436
405
|
return False
|
|
437
406
|
|
|
438
407
|
def get_account_data(self, user_mail: str) -> list[dict]:
|
|
439
|
-
jira_rst_url =
|
|
440
|
-
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
|
+
)
|
|
441
411
|
response = requests.get(jira_rst_url)
|
|
442
412
|
return response.json()
|
|
443
413
|
|
|
444
414
|
def get_account_id(self, user_mail: str) -> str:
|
|
445
415
|
data = self.get_account_data(user_mail=user_mail)
|
|
446
|
-
return data[0][
|
|
416
|
+
return data[0]["accountId"]
|
|
447
417
|
|
|
448
418
|
def get_account_display_name(self, user_display_name: str) -> str:
|
|
449
419
|
data = self.get_account_data(user_mail=user_display_name)
|
|
450
|
-
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.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-1.0.15}/python_jira_plus.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|