python-jira-plus 0.2.1__tar.gz → 1.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-0.2.1 → python_jira_plus-1.0.1}/MANIFEST.in +1 -0
- {python_jira_plus-0.2.1/python_jira_plus.egg-info → python_jira_plus-1.0.1}/PKG-INFO +11 -14
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1/python_jira_plus.egg-info}/PKG-INFO +11 -14
- python_jira_plus-1.0.1/python_jira_plus.egg-info/SOURCES.txt +11 -0
- python_jira_plus-1.0.1/python_jira_plus.egg-info/requires.txt +8 -0
- python_jira_plus-1.0.1/python_jira_plus.egg-info/top_level.txt +1 -0
- python_jira_plus-1.0.1/requirements.txt +8 -0
- python_jira_plus-1.0.1/setup.py +36 -0
- python_jira_plus-0.2.1/python_jira_plus/__init__.py +0 -33
- python_jira_plus-0.2.1/python_jira_plus/describe_allowed_value.py +0 -38
- python_jira_plus-0.2.1/python_jira_plus/jira_plus.py +0 -396
- python_jira_plus-0.2.1/python_jira_plus.egg-info/SOURCES.txt +0 -18
- python_jira_plus-0.2.1/python_jira_plus.egg-info/requires.txt +0 -3
- python_jira_plus-0.2.1/python_jira_plus.egg-info/top_level.txt +0 -2
- python_jira_plus-0.2.1/setup.py +0 -33
- python_jira_plus-0.2.1/usage_example/__init__.py +0 -0
- python_jira_plus-0.2.1/usage_example/create_jira_issue.py +0 -26
- python_jira_plus-0.2.1/usage_example/featch_jira_issues.py +0 -28
- python_jira_plus-0.2.1/usage_example/get_jira_issue.py +0 -24
- python_jira_plus-0.2.1/usage_example/update_jira_issue.py +0 -28
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/LICENSE +0 -0
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/README.md +0 -0
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/pyproject.toml +0 -0
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/python_jira_plus.egg-info/dependency_links.txt +0 -0
- {python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/setup.cfg +0 -0
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
7
7
|
Author-email:
|
|
8
8
|
Project-URL: Repository, https://github.com/aviz92/python-jira-plus
|
|
9
|
-
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-Python: >=3.9
|
|
9
|
+
Requires-Python: >=3.11
|
|
16
10
|
Description-Content-Type: text/markdown
|
|
17
11
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: setuptools
|
|
13
|
+
Requires-Dist: wheel
|
|
14
|
+
Requires-Dist: dotenv
|
|
15
|
+
Requires-Dist: custom-python-logger
|
|
16
|
+
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: retrying
|
|
19
|
+
Requires-Dist: jira~=3.8.0
|
|
21
20
|
Dynamic: author
|
|
22
|
-
Dynamic: classifier
|
|
23
21
|
Dynamic: description
|
|
24
22
|
Dynamic: description-content-type
|
|
25
23
|
Dynamic: home-page
|
|
26
|
-
Dynamic: keywords
|
|
27
24
|
Dynamic: license-file
|
|
28
25
|
Dynamic: project-url
|
|
29
26
|
Dynamic: requires-dist
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jira-plus
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
7
7
|
Author-email:
|
|
8
8
|
Project-URL: Repository, https://github.com/aviz92/python-jira-plus
|
|
9
|
-
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-Python: >=3.9
|
|
9
|
+
Requires-Python: >=3.11
|
|
16
10
|
Description-Content-Type: text/markdown
|
|
17
11
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: setuptools
|
|
13
|
+
Requires-Dist: wheel
|
|
14
|
+
Requires-Dist: dotenv
|
|
15
|
+
Requires-Dist: custom-python-logger
|
|
16
|
+
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: retrying
|
|
19
|
+
Requires-Dist: jira~=3.8.0
|
|
21
20
|
Dynamic: author
|
|
22
|
-
Dynamic: classifier
|
|
23
21
|
Dynamic: description
|
|
24
22
|
Dynamic: description-content-type
|
|
25
23
|
Dynamic: home-page
|
|
26
|
-
Dynamic: keywords
|
|
27
24
|
Dynamic: license-file
|
|
28
25
|
Dynamic: project-url
|
|
29
26
|
Dynamic: requires-dist
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
setup.py
|
|
7
|
+
python_jira_plus.egg-info/PKG-INFO
|
|
8
|
+
python_jira_plus.egg-info/SOURCES.txt
|
|
9
|
+
python_jira_plus.egg-info/dependency_links.txt
|
|
10
|
+
python_jira_plus.egg-info/requires.txt
|
|
11
|
+
python_jira_plus.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
package_version = '1.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_long_description_content_type = 'text/markdown'
|
|
9
|
+
package_url = f'https://github.com/aviz92/{package_name}'
|
|
10
|
+
package_python_requires = '>=3.11'
|
|
11
|
+
package_author = 'Avi Zaguri'
|
|
12
|
+
|
|
13
|
+
with open('requirements.txt', 'r') as file:
|
|
14
|
+
package_install_requires = [
|
|
15
|
+
line.strip() for line in file.readlines() if line.strip() and not line.startswith('#')
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
with open('README.md', 'r') as file:
|
|
19
|
+
package_long_description = file.read()
|
|
20
|
+
|
|
21
|
+
setup(
|
|
22
|
+
name=package_name,
|
|
23
|
+
version=package_version,
|
|
24
|
+
packages=find_packages(include=[package_name, f'{package_name}.*']),
|
|
25
|
+
install_requires=package_install_requires,
|
|
26
|
+
author=package_author,
|
|
27
|
+
author_email='',
|
|
28
|
+
description=package_description,
|
|
29
|
+
long_description=package_long_description,
|
|
30
|
+
long_description_content_type=package_long_description_content_type,
|
|
31
|
+
url=package_url,
|
|
32
|
+
project_urls={
|
|
33
|
+
'Repository': package_url,
|
|
34
|
+
},
|
|
35
|
+
python_requires=package_python_requires,
|
|
36
|
+
)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# python-jira-plus
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
Python JIRA Plus
|
|
5
|
-
|
|
6
|
-
An enhanced Python client for JIRA with better error handling,
|
|
7
|
-
pagination, metadata validation, and more.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from dotenv import load_dotenv
|
|
11
|
-
load_dotenv()
|
|
12
|
-
|
|
13
|
-
from python_jira_plus.jira_plus import JiraPlus, ServerType
|
|
14
|
-
from python_jira_plus.describe_allowed_value import describe_allowed_value
|
|
15
|
-
|
|
16
|
-
load_dotenv()
|
|
17
|
-
|
|
18
|
-
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'
|
|
32
|
-
]
|
|
33
|
-
__all__ = ['JiraPlus', 'ServerType', 'BASIC_FIELDS']
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# describe_allowed_value.py
|
|
2
|
-
|
|
3
|
-
from functools import singledispatch
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@singledispatch
|
|
7
|
-
def describe_allowed_value(value, allowed_values: set = None):
|
|
8
|
-
raise NotImplementedError(f"cannot describe this object of type {type(value)}")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@describe_allowed_value.register
|
|
12
|
-
def _(value: int, allowed_values: list = None):
|
|
13
|
-
return value == allowed_values
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@describe_allowed_value.register
|
|
17
|
-
def _(value: float, allowed_values: list = None):
|
|
18
|
-
return value == allowed_values
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@describe_allowed_value.register
|
|
22
|
-
def _(value: str, allowed_values: list = None):
|
|
23
|
-
return value == allowed_values
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@describe_allowed_value.register
|
|
27
|
-
def _(value: list, allowed_values: list = None):
|
|
28
|
-
return any(v in allowed_values for v in value)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@describe_allowed_value.register
|
|
32
|
-
def _(value: set, allowed_values: list = None):
|
|
33
|
-
return any(v in allowed_values for v in value)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@describe_allowed_value.register
|
|
37
|
-
def _(value: dict, allowed_values: list = None):
|
|
38
|
-
return any(v in allowed_values for v in value.values())
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
# jira_plus.py
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import time
|
|
7
|
-
from enum import Enum
|
|
8
|
-
from typing import Any, Union, Optional
|
|
9
|
-
import requests
|
|
10
|
-
from jira import JIRA, JIRAError, Issue
|
|
11
|
-
from jira.client import ResultList
|
|
12
|
-
from retrying import retry
|
|
13
|
-
|
|
14
|
-
from python_jira_plus.describe_allowed_value import describe_allowed_value
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ServerType(Enum):
|
|
18
|
-
CLOUD = 'cloud'
|
|
19
|
-
ON_PREMISE = 'on_premise'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class JiraPlus:
|
|
23
|
-
def __init__(
|
|
24
|
-
self, base_url: Optional[str] = None,
|
|
25
|
-
server_type: ServerType = ServerType.CLOUD,
|
|
26
|
-
urllib3_log_level: int = logging.WARNING
|
|
27
|
-
) -> None:
|
|
28
|
-
self.logger = logging.getLogger()
|
|
29
|
-
logging.getLogger("urllib3").setLevel(urllib3_log_level)
|
|
30
|
-
|
|
31
|
-
self.jira_username = os.getenv('JIRA_USER_NAME')
|
|
32
|
-
self.jira_token = os.getenv('JIRA_TOKEN')
|
|
33
|
-
|
|
34
|
-
if not base_url:
|
|
35
|
-
self.base_url = os.getenv('JIRA_BASE_URL')
|
|
36
|
-
|
|
37
|
-
self.server_type = server_type
|
|
38
|
-
self.server = f'https://{self.base_url}'
|
|
39
|
-
|
|
40
|
-
self.jira_client = self.create_connection()
|
|
41
|
-
self.check_client_connection()
|
|
42
|
-
|
|
43
|
-
@retry(stop_max_attempt_number=3,
|
|
44
|
-
wait_fixed=180000) # Retry 3 times with a 3-min (180000) delay between each attempt
|
|
45
|
-
def create_connection(self) -> Optional[JIRA]:
|
|
46
|
-
jira_client = JIRA(
|
|
47
|
-
basic_auth=(self.jira_username, self.jira_token),
|
|
48
|
-
options={'server': self.server},
|
|
49
|
-
timeout=580
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
if jira_client.projects():
|
|
53
|
-
self.logger.info(f"Jira Connection Successful")
|
|
54
|
-
return jira_client
|
|
55
|
-
else:
|
|
56
|
-
self.logger.error(f"Jira Connection Failed")
|
|
57
|
-
raise JIRAError("Jira Connection Failed")
|
|
58
|
-
|
|
59
|
-
def close_connection(self) -> None:
|
|
60
|
-
try:
|
|
61
|
-
self.jira_client.close()
|
|
62
|
-
except JIRAError as err:
|
|
63
|
-
self.logger.exception(f"JIRAError \n{err}")
|
|
64
|
-
except Exception as err:
|
|
65
|
-
self.logger.exception(f"Exception \n{err}")
|
|
66
|
-
|
|
67
|
-
self.check_client_connection()
|
|
68
|
-
|
|
69
|
-
def check_client_connection(self) -> bool:
|
|
70
|
-
try:
|
|
71
|
-
if self.jira_client.projects():
|
|
72
|
-
return True
|
|
73
|
-
else:
|
|
74
|
-
self.logger.info("Jira client is closed")
|
|
75
|
-
return False
|
|
76
|
-
except JIRAError as err:
|
|
77
|
-
self.logger.exception(f"JIRAError \n{err}")
|
|
78
|
-
return False
|
|
79
|
-
except Exception as err:
|
|
80
|
-
self.logger.exception(f"Exception \n{err}")
|
|
81
|
-
return False
|
|
82
|
-
|
|
83
|
-
def get_issue_by_key(
|
|
84
|
-
self,
|
|
85
|
-
key: str,
|
|
86
|
-
specific_fields: str | list[str] = '*all',
|
|
87
|
-
json_result: bool = True
|
|
88
|
-
) -> None | Issue | dict[str, Any]:
|
|
89
|
-
try:
|
|
90
|
-
jira_issue = self.jira_client.issue(id=key, fields=specific_fields)
|
|
91
|
-
if json_result:
|
|
92
|
-
return jira_issue.raw
|
|
93
|
-
return jira_issue
|
|
94
|
-
except JIRAError as err:
|
|
95
|
-
self.logger.exception(f"JIRAError \n{err}")
|
|
96
|
-
return None
|
|
97
|
-
except Exception as err:
|
|
98
|
-
self.logger.exception(f"Exception \n{err}")
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
def get_objects_by_query(
|
|
102
|
-
self,
|
|
103
|
-
query: str,
|
|
104
|
-
max_results: int = 5000,
|
|
105
|
-
specific_fields: Union[str, list[str]] = "*all",
|
|
106
|
-
jira_err_count: int = 3,
|
|
107
|
-
json_result: bool = True,
|
|
108
|
-
) -> Optional[Union[ResultList[Issue], dict]]:
|
|
109
|
-
return self._paginate_query(
|
|
110
|
-
query=query,
|
|
111
|
-
max_results=max_results,
|
|
112
|
-
specific_fields=specific_fields,
|
|
113
|
-
jira_err_limit=jira_err_count,
|
|
114
|
-
json_result=json_result,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def _paginate_query(
|
|
118
|
-
self,
|
|
119
|
-
query: str,
|
|
120
|
-
max_results: int,
|
|
121
|
-
specific_fields: Union[str, list[str]],
|
|
122
|
-
jira_err_limit: int,
|
|
123
|
-
json_result: bool
|
|
124
|
-
) -> Union[ResultList[Issue] | list]:
|
|
125
|
-
start_at = 0
|
|
126
|
-
all_issues = []
|
|
127
|
-
retries = 0
|
|
128
|
-
while True:
|
|
129
|
-
try:
|
|
130
|
-
self.logger.debug(f"Fetching issues from startAt={start_at}")
|
|
131
|
-
page = self.jira_client.search_issues(
|
|
132
|
-
jql_str=query,
|
|
133
|
-
startAt=start_at,
|
|
134
|
-
maxResults=max_results,
|
|
135
|
-
fields=specific_fields,
|
|
136
|
-
json_result=json_result
|
|
137
|
-
)
|
|
138
|
-
all_issues += page['issues'] if json_result else page
|
|
139
|
-
if json_result:
|
|
140
|
-
start_at += page['maxResults']
|
|
141
|
-
else:
|
|
142
|
-
start_at += page.maxResults
|
|
143
|
-
if start_at >= max_results:
|
|
144
|
-
break
|
|
145
|
-
retries = 0 # reset on success
|
|
146
|
-
except JIRAError as err:
|
|
147
|
-
retries += 1
|
|
148
|
-
self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
|
|
149
|
-
if retries >= jira_err_limit:
|
|
150
|
-
self.logger.exception("Max retries exceeded.")
|
|
151
|
-
return []
|
|
152
|
-
time.sleep(300 if retries == jira_err_limit - 1 else 90)
|
|
153
|
-
return all_issues
|
|
154
|
-
|
|
155
|
-
def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
|
|
156
|
-
meta = self.jira_client.createmeta(
|
|
157
|
-
projectKeys=project_key,
|
|
158
|
-
issuetypeNames=issue_type,
|
|
159
|
-
expand="projects.issuetypes.fields"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
project_meta = next((p for p in meta['projects'] if p['key'] == project_key), None)
|
|
163
|
-
if not project_meta:
|
|
164
|
-
raise ValueError(f"Project {project_key} not found in metadata")
|
|
165
|
-
|
|
166
|
-
issue_meta = next(
|
|
167
|
-
(it for it in project_meta['issuetypes'] if it['name'].lower() == issue_type.lower()),
|
|
168
|
-
None
|
|
169
|
-
)
|
|
170
|
-
if not issue_meta:
|
|
171
|
-
raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
|
|
172
|
-
return meta, issue_meta
|
|
173
|
-
|
|
174
|
-
def get_allowed_values(
|
|
175
|
-
self,
|
|
176
|
-
project_key: str,
|
|
177
|
-
issue_type: str,
|
|
178
|
-
field_id_or_name: str,
|
|
179
|
-
) -> Optional[list]:
|
|
180
|
-
"""
|
|
181
|
-
Returns the allowed values for a specific field in a project + issue type context.
|
|
182
|
-
"""
|
|
183
|
-
_, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
|
|
184
|
-
|
|
185
|
-
# Try to find by ID or fallback to case-insensitive name
|
|
186
|
-
field = issue_meta['fields'].get(field_id_or_name)
|
|
187
|
-
if not field:
|
|
188
|
-
# Try matching by name (case-insensitive)
|
|
189
|
-
for fid, fmeta in issue_meta['fields'].items():
|
|
190
|
-
if fmeta['name'].lower() == field_id_or_name.lower():
|
|
191
|
-
field = fmeta
|
|
192
|
-
break
|
|
193
|
-
|
|
194
|
-
if not field:
|
|
195
|
-
raise ValueError(
|
|
196
|
-
f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'")
|
|
197
|
-
|
|
198
|
-
allowed = field.get('allowedValues')
|
|
199
|
-
if not allowed:
|
|
200
|
-
self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
|
|
201
|
-
return None
|
|
202
|
-
|
|
203
|
-
return allowed
|
|
204
|
-
|
|
205
|
-
def _is_value_allowed(self, value: Any, allowed_values: list[Any]) -> bool:
|
|
206
|
-
allowed_names = {av.get('name') for av in allowed_values}
|
|
207
|
-
allowed_keys = {av.get('key') for av in allowed_values}
|
|
208
|
-
allowed_values = allowed_names | allowed_keys
|
|
209
|
-
return describe_allowed_value(value, allowed_values=allowed_values)
|
|
210
|
-
|
|
211
|
-
def validate_fields(
|
|
212
|
-
self,
|
|
213
|
-
project_key: str,
|
|
214
|
-
issue_type: str,
|
|
215
|
-
fields: Optional[dict] = None
|
|
216
|
-
) -> None:
|
|
217
|
-
meta, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
|
|
218
|
-
field_types = meta['projects'][0]['issuetypes'][0]['fields']
|
|
219
|
-
for custom_field, value in fields.items():
|
|
220
|
-
if custom_field not in issue_meta['fields']:
|
|
221
|
-
raise ValueError(
|
|
222
|
-
f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
if allowed_value := issue_meta['fields'][custom_field].get('allowedValues'):
|
|
226
|
-
if not self._is_value_allowed(value=value, allowed_values=allowed_value):
|
|
227
|
-
raise ValueError(
|
|
228
|
-
f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}' with issue type '{issue_type}'."
|
|
229
|
-
f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
field_type = field_types[custom_field]['schema']['type']
|
|
233
|
-
if field_type == 'string' and not isinstance(value, str):
|
|
234
|
-
raise ValueError(f"Field '{custom_field}' must be a string")
|
|
235
|
-
elif field_type == 'number' and not isinstance(value, (int, float)):
|
|
236
|
-
raise ValueError(f"Field '{custom_field}' must be a number")
|
|
237
|
-
elif field_type == 'boolean' and not isinstance(value, bool):
|
|
238
|
-
raise ValueError(f"Field '{custom_field}' must be a boolean")
|
|
239
|
-
elif field_type == 'array' and not isinstance(value, list):
|
|
240
|
-
raise ValueError(f"Field '{custom_field}' must be an array")
|
|
241
|
-
elif field_type == 'object' and not isinstance(value, dict):
|
|
242
|
-
raise ValueError(f"Field '{custom_field}' must be an object")
|
|
243
|
-
elif field_type == 'issuetype' and not isinstance(value, dict):
|
|
244
|
-
raise ValueError(f"Field '{custom_field}' must be an issue type")
|
|
245
|
-
# Add more field type checks as needed
|
|
246
|
-
return issue_meta['fields'].keys()
|
|
247
|
-
|
|
248
|
-
def create_issue(
|
|
249
|
-
self,
|
|
250
|
-
project_key: str,
|
|
251
|
-
summary: str,
|
|
252
|
-
description: str,
|
|
253
|
-
issue_type: str,
|
|
254
|
-
assignee: Optional[str] = None,
|
|
255
|
-
custom_fields: Optional[dict] = None,
|
|
256
|
-
) -> Optional[Issue]:
|
|
257
|
-
issue_fields = {
|
|
258
|
-
"project": {"key": project_key},
|
|
259
|
-
"issuetype": {"name": issue_type},
|
|
260
|
-
"summary": summary,
|
|
261
|
-
"description": description,
|
|
262
|
-
}
|
|
263
|
-
if assignee:
|
|
264
|
-
issue_fields["assignee"] = {"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
|
|
265
|
-
|
|
266
|
-
if custom_fields:
|
|
267
|
-
issue_fields = issue_fields | custom_fields
|
|
268
|
-
self.validate_fields(project_key=project_key, issue_type=issue_type, fields=issue_fields)
|
|
269
|
-
|
|
270
|
-
try:
|
|
271
|
-
issue = self.jira_client.create_issue(fields=issue_fields)
|
|
272
|
-
self.logger.info(f"Issue {issue.key} created successfully.")
|
|
273
|
-
return issue
|
|
274
|
-
except JIRAError as err:
|
|
275
|
-
self.logger.error(f"JIRAError while creating issue: {err.text}")
|
|
276
|
-
return None
|
|
277
|
-
except Exception as err:
|
|
278
|
-
self.logger.exception(f"Unexpected error: {err}")
|
|
279
|
-
return None
|
|
280
|
-
|
|
281
|
-
def update_issue(
|
|
282
|
-
self,
|
|
283
|
-
issue_key: str,
|
|
284
|
-
fields_to_update: dict
|
|
285
|
-
) -> Optional[Issue]:
|
|
286
|
-
try:
|
|
287
|
-
issue = self.jira_client.issue(issue_key)
|
|
288
|
-
self.validate_fields(
|
|
289
|
-
project_key=issue.fields.project.key,
|
|
290
|
-
issue_type=issue.fields.issuetype.name,
|
|
291
|
-
fields=fields_to_update
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
issue.update(fields=fields_to_update)
|
|
295
|
-
self.logger.info(f"Issue {issue.key} updated successfully.")
|
|
296
|
-
return issue
|
|
297
|
-
except JIRAError as err:
|
|
298
|
-
self.logger.error(f"JIRAError while updating issue: {err.text}")
|
|
299
|
-
return None
|
|
300
|
-
except Exception as err:
|
|
301
|
-
self.logger.exception(f"Unexpected error: {err}")
|
|
302
|
-
return None
|
|
303
|
-
|
|
304
|
-
def delete_issue(self, issue_key: str) -> bool:
|
|
305
|
-
try:
|
|
306
|
-
issue = self.get_issue_by_key(key=issue_key)
|
|
307
|
-
issue.delete()
|
|
308
|
-
self.logger.info(f"Issue {issue_key} deleted successfully.")
|
|
309
|
-
return True
|
|
310
|
-
except JIRAError as err:
|
|
311
|
-
self.logger.error(f"JIRAError while deleting issue: {err.text}")
|
|
312
|
-
return False
|
|
313
|
-
except Exception as err:
|
|
314
|
-
self.logger.exception(f"Unexpected error: {err}")
|
|
315
|
-
return False
|
|
316
|
-
|
|
317
|
-
def _get_available_link_types(self) -> set[str]:
|
|
318
|
-
try:
|
|
319
|
-
return {link_type.name for link_type in self.jira_client.issue_link_types()}
|
|
320
|
-
except Exception as err:
|
|
321
|
-
self.logger.exception(f"Failed to retrieve link types from JIRA. \n{err}")
|
|
322
|
-
return set()
|
|
323
|
-
|
|
324
|
-
def create_link(self, issue_key: str, link_type: str, in_issue_key: str) -> bool:
|
|
325
|
-
available_link_types = self._get_available_link_types()
|
|
326
|
-
if link_type not in available_link_types:
|
|
327
|
-
raise ValueError(f"Invalid link type '{link_type}'. Available link types: {available_link_types}")
|
|
328
|
-
|
|
329
|
-
try:
|
|
330
|
-
self.jira_client.create_issue_link(
|
|
331
|
-
type=link_type,
|
|
332
|
-
inwardIssue=issue_key,
|
|
333
|
-
outwardIssue=in_issue_key,
|
|
334
|
-
)
|
|
335
|
-
self.logger.info(f"Link between {issue_key} and {in_issue_key} was created")
|
|
336
|
-
return True
|
|
337
|
-
except JIRAError as err:
|
|
338
|
-
self.logger.exception(f"JIRAError - Link between {issue_key} and {in_issue_key} was not created\n{err}")
|
|
339
|
-
return False
|
|
340
|
-
except Exception as err:
|
|
341
|
-
self.logger.exception(f"Exception - Link between {issue_key} and {in_issue_key} was not created\n{err}")
|
|
342
|
-
return False
|
|
343
|
-
|
|
344
|
-
def transition_issue(
|
|
345
|
-
self,
|
|
346
|
-
issue_key: str,
|
|
347
|
-
transition_name: str,
|
|
348
|
-
fields: Optional[dict] = None,
|
|
349
|
-
comment: Optional[str] = None,
|
|
350
|
-
) -> bool:
|
|
351
|
-
try:
|
|
352
|
-
transitions = self.jira_client.transitions(issue_key)
|
|
353
|
-
transition_names = {t['name'] for t in transitions}
|
|
354
|
-
if transition_name not in transition_names:
|
|
355
|
-
self.logger.error(
|
|
356
|
-
f"Transition '{transition_name}' not found for issue {issue_key}. "
|
|
357
|
-
f"\nAvailable transitions: {transition_names}"
|
|
358
|
-
)
|
|
359
|
-
return False
|
|
360
|
-
|
|
361
|
-
transition_id = next(
|
|
362
|
-
(t['id'] for t in transitions if t['name'].lower() == transition_name.lower()),
|
|
363
|
-
None
|
|
364
|
-
)
|
|
365
|
-
if not transition_id:
|
|
366
|
-
self.logger.error(f"Transition '{transition_name}' not found for issue {issue_key}")
|
|
367
|
-
return False
|
|
368
|
-
|
|
369
|
-
self.jira_client.transition_issue(
|
|
370
|
-
issue=issue_key,
|
|
371
|
-
transition=transition_id,
|
|
372
|
-
fields=fields or {},
|
|
373
|
-
comment=comment or None
|
|
374
|
-
)
|
|
375
|
-
self.logger.info(f"Issue {issue_key} transitioned to '{transition_name}'")
|
|
376
|
-
return True
|
|
377
|
-
except JIRAError as err:
|
|
378
|
-
self.logger.exception(f"JIRAError during transition of {issue_key}: {err}")
|
|
379
|
-
return False
|
|
380
|
-
except Exception as err:
|
|
381
|
-
self.logger.exception(f"Unexpected error during transition of {issue_key}: {err}")
|
|
382
|
-
return False
|
|
383
|
-
|
|
384
|
-
def get_account_data(self, user_mail: str) -> list[dict]:
|
|
385
|
-
jira_rst_url = \
|
|
386
|
-
f'https://{self.jira_username}:{self.jira_token}@{self.base_url}/rest/api/{3}/user/search?query={user_mail}'
|
|
387
|
-
response = requests.get(jira_rst_url)
|
|
388
|
-
return response.json()
|
|
389
|
-
|
|
390
|
-
def get_account_id(self, user_mail: str) -> str:
|
|
391
|
-
data = self.get_account_data(user_mail=user_mail)
|
|
392
|
-
return data[0]['accountId']
|
|
393
|
-
|
|
394
|
-
def get_account_display_name(self, user_display_name: str) -> str:
|
|
395
|
-
data = self.get_account_data(user_mail=user_display_name)
|
|
396
|
-
return data[0]['displayName']
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
setup.py
|
|
6
|
-
python_jira_plus/__init__.py
|
|
7
|
-
python_jira_plus/describe_allowed_value.py
|
|
8
|
-
python_jira_plus/jira_plus.py
|
|
9
|
-
python_jira_plus.egg-info/PKG-INFO
|
|
10
|
-
python_jira_plus.egg-info/SOURCES.txt
|
|
11
|
-
python_jira_plus.egg-info/dependency_links.txt
|
|
12
|
-
python_jira_plus.egg-info/requires.txt
|
|
13
|
-
python_jira_plus.egg-info/top_level.txt
|
|
14
|
-
usage_example/__init__.py
|
|
15
|
-
usage_example/create_jira_issue.py
|
|
16
|
-
usage_example/featch_jira_issues.py
|
|
17
|
-
usage_example/get_jira_issue.py
|
|
18
|
-
usage_example/update_jira_issue.py
|
python_jira_plus-0.2.1/setup.py
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
-
long_description = fh.read()
|
|
5
|
-
|
|
6
|
-
setup(
|
|
7
|
-
name="python-jira-plus",
|
|
8
|
-
version="0.2.1",
|
|
9
|
-
author='Avi Zaguri',
|
|
10
|
-
author_email="",
|
|
11
|
-
description="Enhanced Python client for JIRA with better error handling, pagination, and metadata validation",
|
|
12
|
-
long_description=long_description,
|
|
13
|
-
long_description_content_type="text/markdown",
|
|
14
|
-
url="https://github.com/aviz92/python-jira-plus",
|
|
15
|
-
project_urls={
|
|
16
|
-
'Repository': 'https://github.com/aviz92/python-jira-plus',
|
|
17
|
-
},
|
|
18
|
-
packages=find_packages(),
|
|
19
|
-
classifiers=[
|
|
20
|
-
"Development Status :: 4 - Beta",
|
|
21
|
-
"Intended Audience :: Developers",
|
|
22
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
-
"License :: OSI Approved :: MIT License",
|
|
24
|
-
"Programming Language :: Python :: 3",
|
|
25
|
-
],
|
|
26
|
-
python_requires=">=3.9",
|
|
27
|
-
install_requires=[
|
|
28
|
-
"jira>=3.1.1",
|
|
29
|
-
"retrying>=1.3.3",
|
|
30
|
-
"custom-python-logger>=0.1.4",
|
|
31
|
-
],
|
|
32
|
-
keywords="jira, atlassian, api, client",
|
|
33
|
-
)
|
|
File without changes
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# create_jira_issue.py
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from custom_python_logger.logger import get_logger
|
|
5
|
-
|
|
6
|
-
from python_jira_plus import JiraPlus
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main():
|
|
10
|
-
_ = get_logger(
|
|
11
|
-
project_name='Logger Project Test',
|
|
12
|
-
log_level=logging.DEBUG,
|
|
13
|
-
extra={'user': 'test_user'}
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
jira_plus = JiraPlus()
|
|
17
|
-
_ = jira_plus.create_issue(
|
|
18
|
-
project_key="JIRA TEST",
|
|
19
|
-
issue_type='Story',
|
|
20
|
-
summary='Test issue',
|
|
21
|
-
description='This is a test issue.',
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if __name__ == '__main__':
|
|
26
|
-
main()
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# featch_jira_issue.py
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from custom_python_logger.logger import get_logger
|
|
5
|
-
|
|
6
|
-
from python_jira_plus import BASIC_FIELDS
|
|
7
|
-
from python_jira_plus.jira_plus import JiraPlus
|
|
8
|
-
|
|
9
|
-
QUERY = 'project = "JIRA TEST" AND issuetype = Story'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main():
|
|
13
|
-
_ = get_logger(
|
|
14
|
-
project_name='Logger Project Test',
|
|
15
|
-
log_level=logging.DEBUG,
|
|
16
|
-
extra={'user': 'test_user'}
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
jira_plus = JiraPlus()
|
|
20
|
-
_ = jira_plus.get_objects_by_query(
|
|
21
|
-
query=QUERY,
|
|
22
|
-
specific_fields=BASIC_FIELDS,
|
|
23
|
-
max_results=300,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if __name__ == '__main__':
|
|
28
|
-
main()
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# get_jira_issue.py
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from custom_python_logger.logger import get_logger
|
|
5
|
-
|
|
6
|
-
from python_jira_plus.jira_plus import JiraPlus
|
|
7
|
-
|
|
8
|
-
ISSUE_KEY = 'Test-123'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main():
|
|
12
|
-
_ = get_logger(
|
|
13
|
-
project_name='Logger Project Test',
|
|
14
|
-
log_level=logging.DEBUG,
|
|
15
|
-
extra={'user': 'test_user'}
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
jira_plus = JiraPlus()
|
|
19
|
-
_ = jira_plus.get_issue_by_key(key=ISSUE_KEY)
|
|
20
|
-
print()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if __name__ == '__main__':
|
|
24
|
-
main()
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# update_jira_issue.py
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from custom_python_logger.logger import get_logger
|
|
5
|
-
|
|
6
|
-
from python_jira_plus.jira_plus import JiraPlus
|
|
7
|
-
|
|
8
|
-
ISSUE_KEY = 'Test-123'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main():
|
|
12
|
-
_ = get_logger(
|
|
13
|
-
project_name='Logger Project Test',
|
|
14
|
-
log_level=logging.DEBUG,
|
|
15
|
-
extra={'user': 'test_user'}
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
jira_plus = JiraPlus()
|
|
19
|
-
_ = jira_plus.update_issue(
|
|
20
|
-
issue_key=ISSUE_KEY,
|
|
21
|
-
fields_to_update={
|
|
22
|
-
'priority': {'name': 'Critical'},
|
|
23
|
-
}
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if __name__ == '__main__':
|
|
28
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_jira_plus-0.2.1 → python_jira_plus-1.0.1}/python_jira_plus.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|