python-jira-plus 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Avi Zaguri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-jira-plus
3
+ Version: 0.1.0
4
+ Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation
5
+ Home-page: https://github.com/yourusername/python-jira-plus
6
+ Author: Avi Zaguri
7
+ Author-email:
8
+ Project-URL: Repository, https://github.com/aviz92/python-jira-plus
9
+ Keywords: jira,atlassian,api,client
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
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: jira>=3.1.1
19
+ Requires-Dist: retrying>=1.3.3
20
+ Requires-Dist: python-jira-plus>=0.1.4
21
+ Dynamic: author
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: project-url
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # Python JIRA Plus
34
+
35
+ [![PyPI version](https://badge.fury.io/py/python-jira-plus.svg)](https://badge.fury.io/py/python-jira-plus)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+
38
+ An enhanced Python client for JIRA that extends the functionality of the official `jira` package, providing better error handling, pagination, metadata validation, and more.
39
+
40
+ ---
41
+
42
+ ## Features
43
+ - ✅ Simplified connection to JIRA Cloud and On-Premise instances
44
+ - ✅ Robust error handling with automatic retries
45
+ - ✅ Built-in pagination for large result sets
46
+ - ✅ Field validation against JIRA metadata
47
+ - ✅ Enhanced issue creation, retrieval, and updating
48
+ - ✅ Support for allowed values validation
49
+
50
+ ---
51
+
52
+ ## Installation
53
+ ```bash
54
+ pip install python-jira-plus
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+ - Python 3.9+
61
+ - `jira` package
62
+ - `retrying` package
63
+ - `custom-python-logger` package
64
+
65
+ ---
66
+
67
+ ## Configuration
68
+ The package uses environment variables for authentication and configuration:
69
+
70
+ ```bash
71
+ # Required environment variables
72
+ JIRA_USER_NAME=your_jira_username
73
+ JIRA_TOKEN=your_jira_api_token
74
+ JIRA_BASE_URL=your-instance.atlassian.net # Only used if base_url is not provided to constructor
75
+ ```
76
+
77
+ ## Examples
78
+
79
+ ### Creating an Issue with Custom Fields
80
+ ```python
81
+ from python_jira_plus.jira_plus import JiraPlus
82
+
83
+ jira_client = JiraPlus()
84
+ issue = jira_client.create_issue(
85
+ project_key="PROJ",
86
+ summary="Implement new feature",
87
+ description="This feature will improve performance",
88
+ issue_type="Task",
89
+ custom_fields={
90
+ "priority": "Critical", # Priority
91
+ "customfield_10003": {"name": "Sprint 1"} # Sprint
92
+ }
93
+ )
94
+ ```
95
+
96
+ ### Searching for Issues
97
+ ```python
98
+ from python_jira_plus.jira_plus import JiraPlus
99
+
100
+ jira_client = JiraPlus()
101
+ issues = jira_client.get_objects_by_query(
102
+ query="project = PROJ AND status = 'In Progress' ORDER BY created DESC",
103
+ max_results=50,
104
+ specific_fields=["summary", "status", "assignee"],
105
+ json_result=False
106
+ )
107
+
108
+ for issue in issues:
109
+ print(f"{issue.key}: {issue.fields.summary} - {issue.fields.status.name}")
110
+ ```
111
+
112
+ ### Updating an Issue
113
+ ```python
114
+ from python_jira_plus.jira_plus import JiraPlus
115
+
116
+ jira_client = JiraPlus()
117
+ issue = jira_client.get_issue_by_key(key="PROJ-123", json_result=False)
118
+
119
+ fields_to_update = {
120
+ "summary": "Updated summary",
121
+ "description": "Updated description",
122
+ "customfield_10003": {"name": "Sprint 2"}, # Update Sprint
123
+ }
124
+ _ = jira_client.update_issue(
125
+ issue_key=issue.key,
126
+ fields_to_update=fields_to_update
127
+ )
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 🤝 Contributing
133
+ If you have a helpful tool, pattern, or improvement to suggest:
134
+ Fork the repo
135
+ Create a new branch
136
+ Submit a pull request
137
+ I welcome additions that promote clean, productive, and maintainable development.
138
+
139
+ ---
140
+
141
+ ## 🙏 Thanks
142
+ Thanks for exploring this repository!
143
+ Happy coding!
@@ -0,0 +1,111 @@
1
+ # Python JIRA Plus
2
+
3
+ [![PyPI version](https://badge.fury.io/py/python-jira-plus.svg)](https://badge.fury.io/py/python-jira-plus)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ An enhanced Python client for JIRA that extends the functionality of the official `jira` package, providing better error handling, pagination, metadata validation, and more.
7
+
8
+ ---
9
+
10
+ ## Features
11
+ - ✅ Simplified connection to JIRA Cloud and On-Premise instances
12
+ - ✅ Robust error handling with automatic retries
13
+ - ✅ Built-in pagination for large result sets
14
+ - ✅ Field validation against JIRA metadata
15
+ - ✅ Enhanced issue creation, retrieval, and updating
16
+ - ✅ Support for allowed values validation
17
+
18
+ ---
19
+
20
+ ## Installation
21
+ ```bash
22
+ pip install python-jira-plus
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Requirements
28
+ - Python 3.9+
29
+ - `jira` package
30
+ - `retrying` package
31
+ - `custom-python-logger` package
32
+
33
+ ---
34
+
35
+ ## Configuration
36
+ The package uses environment variables for authentication and configuration:
37
+
38
+ ```bash
39
+ # Required environment variables
40
+ JIRA_USER_NAME=your_jira_username
41
+ JIRA_TOKEN=your_jira_api_token
42
+ JIRA_BASE_URL=your-instance.atlassian.net # Only used if base_url is not provided to constructor
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ ### Creating an Issue with Custom Fields
48
+ ```python
49
+ from python_jira_plus.jira_plus import JiraPlus
50
+
51
+ jira_client = JiraPlus()
52
+ issue = jira_client.create_issue(
53
+ project_key="PROJ",
54
+ summary="Implement new feature",
55
+ description="This feature will improve performance",
56
+ issue_type="Task",
57
+ custom_fields={
58
+ "priority": "Critical", # Priority
59
+ "customfield_10003": {"name": "Sprint 1"} # Sprint
60
+ }
61
+ )
62
+ ```
63
+
64
+ ### Searching for Issues
65
+ ```python
66
+ from python_jira_plus.jira_plus import JiraPlus
67
+
68
+ jira_client = JiraPlus()
69
+ issues = jira_client.get_objects_by_query(
70
+ query="project = PROJ AND status = 'In Progress' ORDER BY created DESC",
71
+ max_results=50,
72
+ specific_fields=["summary", "status", "assignee"],
73
+ json_result=False
74
+ )
75
+
76
+ for issue in issues:
77
+ print(f"{issue.key}: {issue.fields.summary} - {issue.fields.status.name}")
78
+ ```
79
+
80
+ ### Updating an Issue
81
+ ```python
82
+ from python_jira_plus.jira_plus import JiraPlus
83
+
84
+ jira_client = JiraPlus()
85
+ issue = jira_client.get_issue_by_key(key="PROJ-123", json_result=False)
86
+
87
+ fields_to_update = {
88
+ "summary": "Updated summary",
89
+ "description": "Updated description",
90
+ "customfield_10003": {"name": "Sprint 2"}, # Update Sprint
91
+ }
92
+ _ = jira_client.update_issue(
93
+ issue_key=issue.key,
94
+ fields_to_update=fields_to_update
95
+ )
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 🤝 Contributing
101
+ If you have a helpful tool, pattern, or improvement to suggest:
102
+ Fork the repo
103
+ Create a new branch
104
+ Submit a pull request
105
+ I welcome additions that promote clean, productive, and maintainable development.
106
+
107
+ ---
108
+
109
+ ## 🙏 Thanks
110
+ Thanks for exploring this repository!
111
+ Happy coding!
@@ -0,0 +1,11 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.black]
6
+ line-length = 100
7
+ target-version = ["py39", "py310", "py311"]
8
+
9
+ [tool.isort]
10
+ profile = "black"
11
+ line_length = 100
@@ -0,0 +1,35 @@
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
+ __version__ = '0.1.0'
11
+
12
+ from dotenv import load_dotenv
13
+ load_dotenv()
14
+
15
+ from python_jira_plus.jira_plus import JiraPlus, ServerType
16
+ from python_jira_plus.describe_allowed_value import describe_allowed_value
17
+
18
+ load_dotenv()
19
+
20
+ BASIC_FIELDS = [
21
+ 'created',
22
+ 'updated',
23
+ 'issuetype',
24
+ 'project',
25
+ 'status',
26
+ 'summary',
27
+ 'description',
28
+ 'reporter',
29
+ 'assignee',
30
+ 'priority',
31
+ 'labels',
32
+ 'resolution',
33
+ 'parent'
34
+ ]
35
+ __all__ = ['JiraPlus', 'ServerType', 'BASIC_FIELDS']
@@ -0,0 +1,38 @@
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())
@@ -0,0 +1,314 @@
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
+ from jira import JIRA, JIRAError, Issue
10
+ from jira.client import ResultList
11
+ from retrying import retry
12
+
13
+ from python_jira_plus.describe_allowed_value import describe_allowed_value
14
+
15
+
16
+ class ServerType(Enum):
17
+ CLOUD = 'cloud'
18
+ ON_PREMISE = 'on_premise'
19
+
20
+
21
+ class JiraPlus:
22
+ def __init__(
23
+ self, base_url: Optional[str] = None,
24
+ server_type: ServerType = ServerType.CLOUD,
25
+ urllib3_log_level: int = logging.WARNING
26
+ ) -> None:
27
+ self.logger = logging.getLogger()
28
+ logging.getLogger("urllib3").setLevel(urllib3_log_level)
29
+
30
+ self.jira_username = os.getenv('JIRA_USER_NAME')
31
+ self.jira_token = os.getenv('JIRA_TOKEN')
32
+
33
+ if not base_url:
34
+ self.base_url = os.getenv('JIRA_BASE_URL')
35
+
36
+ self.server_type = server_type
37
+ self.server = f'https://{self.base_url}'
38
+
39
+ self.jira_client = self.create_connection()
40
+ self.check_client_connection()
41
+
42
+ @retry(stop_max_attempt_number=3,
43
+ wait_fixed=180000) # Retry 3 times with a 3-min (180000) delay between each attempt
44
+ def create_connection(self) -> Optional[JIRA]:
45
+ jira_client = JIRA(
46
+ basic_auth=(self.jira_username, self.jira_token),
47
+ options={'server': self.server},
48
+ timeout=580
49
+ )
50
+
51
+ if jira_client.projects():
52
+ self.logger.info(f"Jira Connection Successful")
53
+ return jira_client
54
+ else:
55
+ self.logger.error(f"Jira Connection Failed")
56
+ raise JIRAError("Jira Connection Failed")
57
+
58
+ def close_connection(self) -> None:
59
+ try:
60
+ self.jira_client.close()
61
+ except JIRAError as err:
62
+ self.logger.exception(f"JIRAError \n{err}")
63
+ except Exception as err:
64
+ self.logger.exception(f"Exception \n{err}")
65
+
66
+ self.check_client_connection()
67
+
68
+ def check_client_connection(self) -> bool:
69
+ try:
70
+ if self.jira_client.projects():
71
+ return True
72
+ else:
73
+ self.logger.info("Jira client is closed")
74
+ return False
75
+ except JIRAError as err:
76
+ self.logger.exception(f"JIRAError \n{err}")
77
+ return False
78
+ except Exception as err:
79
+ self.logger.exception(f"Exception \n{err}")
80
+ return False
81
+
82
+ def get_issue_by_key(
83
+ self,
84
+ key: str,
85
+ specific_fields: str | list[str] = '*all',
86
+ json_result: bool = True
87
+ ) -> None | Issue | dict[str, Any]:
88
+ try:
89
+ jira_issue = self.jira_client.issue(id=key, fields=specific_fields)
90
+ if json_result:
91
+ return jira_issue.raw
92
+ return jira_issue
93
+ except JIRAError as err:
94
+ self.logger.exception(f"JIRAError \n{err}")
95
+ return None
96
+ except Exception as err:
97
+ self.logger.exception(f"Exception \n{err}")
98
+ return None
99
+
100
+ def get_objects_by_query(
101
+ self,
102
+ query: str,
103
+ max_results: int = 5000,
104
+ specific_fields: Union[str, list[str]] = "*all",
105
+ jira_err_count: int = 3,
106
+ json_result: bool = True,
107
+ ) -> Optional[Union[ResultList[Issue], dict]]:
108
+ return self._paginate_query(
109
+ query=query,
110
+ max_results=max_results,
111
+ specific_fields=specific_fields,
112
+ jira_err_limit=jira_err_count,
113
+ json_result=json_result,
114
+ )
115
+
116
+ def _paginate_query(
117
+ self,
118
+ query: str,
119
+ max_results: int,
120
+ specific_fields: Union[str, list[str]],
121
+ jira_err_limit: int,
122
+ json_result: bool
123
+ ) -> Union[ResultList[Issue] | list]:
124
+ start_at = 0
125
+ all_issues = []
126
+ retries = 0
127
+ while True:
128
+ try:
129
+ self.logger.debug(f"Fetching issues from startAt={start_at}")
130
+ page = self.jira_client.search_issues(
131
+ jql_str=query,
132
+ startAt=start_at,
133
+ maxResults=max_results,
134
+ fields=specific_fields,
135
+ json_result=json_result
136
+ )
137
+ all_issues += page['issues'] if json_result else page
138
+ if json_result:
139
+ start_at += page['maxResults']
140
+ else:
141
+ start_at += page.maxResults
142
+ if start_at >= max_results:
143
+ break
144
+ retries = 0 # reset on success
145
+ except JIRAError as err:
146
+ retries += 1
147
+ self.logger.warning(f"Retry {retries}/{jira_err_limit} after JIRAError: {err}")
148
+ if retries >= jira_err_limit:
149
+ self.logger.exception("Max retries exceeded.")
150
+ return []
151
+ time.sleep(300 if retries == jira_err_limit - 1 else 90)
152
+ return all_issues
153
+
154
+ def _fetch_metadata(self, project_key: str, issue_type: str) -> tuple[dict, dict]:
155
+ meta = self.jira_client.createmeta(
156
+ projectKeys=project_key,
157
+ issuetypeNames=issue_type,
158
+ expand="projects.issuetypes.fields"
159
+ )
160
+
161
+ project_meta = next((p for p in meta['projects'] if p['key'] == project_key), None)
162
+ if not project_meta:
163
+ raise ValueError(f"Project {project_key} not found in metadata")
164
+
165
+ issue_meta = next(
166
+ (it for it in project_meta['issuetypes'] if it['name'].lower() == issue_type.lower()),
167
+ None
168
+ )
169
+ if not issue_meta:
170
+ raise ValueError(f"Issue type '{issue_type}' not found in project '{project_key}'")
171
+ return meta, issue_meta
172
+
173
+ def get_allowed_values(
174
+ self,
175
+ project_key: str,
176
+ issue_type: str,
177
+ field_id_or_name: str,
178
+ ) -> Optional[list]:
179
+ """
180
+ Returns the allowed values for a specific field in a project + issue type context.
181
+ """
182
+ _, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
183
+
184
+ # Try to find by ID or fallback to case-insensitive name
185
+ field = issue_meta['fields'].get(field_id_or_name)
186
+ if not field:
187
+ # Try matching by name (case-insensitive)
188
+ for fid, fmeta in issue_meta['fields'].items():
189
+ if fmeta['name'].lower() == field_id_or_name.lower():
190
+ field = fmeta
191
+ break
192
+
193
+ if not field:
194
+ raise ValueError(
195
+ f"Field '{field_id_or_name}' not found in project '{project_key}' with issue type '{issue_type}'")
196
+
197
+ allowed = field.get('allowedValues')
198
+ if not allowed:
199
+ self.logger.info(f"No allowed values found for field '{field_id_or_name}'")
200
+ return None
201
+
202
+ return allowed
203
+
204
+ def _is_value_allowed(self, value: Any, allowed_values: list[Any]) -> bool:
205
+ allowed_names = {av.get('name') for av in allowed_values}
206
+ allowed_keys = {av.get('key') for av in allowed_values}
207
+ allowed_values = allowed_names | allowed_keys
208
+ return describe_allowed_value(value, allowed_values=allowed_values)
209
+
210
+ def validate_fields(
211
+ self,
212
+ project_key: str,
213
+ issue_type: str,
214
+ fields: Optional[dict] = None
215
+ ) -> None:
216
+ meta, issue_meta = self._fetch_metadata(project_key=project_key, issue_type=issue_type)
217
+ field_types = meta['projects'][0]['issuetypes'][0]['fields']
218
+ for custom_field, value in fields.items():
219
+ if custom_field not in issue_meta['fields']:
220
+ raise ValueError(
221
+ f'Invalid field(s) for project "{project_key}", issue type "{issue_type}": "{custom_field}"'
222
+ )
223
+
224
+ if allowed_value := issue_meta['fields'][custom_field].get('allowedValues'):
225
+ if not self._is_value_allowed(value=value, allowed_values=allowed_value):
226
+ raise ValueError(
227
+ f"Invalid value '{value}' for field '{custom_field}' in project '{project_key}' with issue type '{issue_type}'."
228
+ f"The allowed values are: {json.dumps(allowed_value, indent=4, sort_keys=True)}"
229
+ )
230
+
231
+ field_type = field_types[custom_field]['schema']['type']
232
+ if field_type == 'string' and not isinstance(value, str):
233
+ raise ValueError(f"Field '{custom_field}' must be a string")
234
+ elif field_type == 'number' and not isinstance(value, (int, float)):
235
+ raise ValueError(f"Field '{custom_field}' must be a number")
236
+ elif field_type == 'boolean' and not isinstance(value, bool):
237
+ raise ValueError(f"Field '{custom_field}' must be a boolean")
238
+ elif field_type == 'array' and not isinstance(value, list):
239
+ raise ValueError(f"Field '{custom_field}' must be an array")
240
+ elif field_type == 'object' and not isinstance(value, dict):
241
+ raise ValueError(f"Field '{custom_field}' must be an object")
242
+ elif field_type == 'issuetype' and not isinstance(value, dict):
243
+ raise ValueError(f"Field '{custom_field}' must be an issue type")
244
+ # Add more field type checks as needed
245
+ return issue_meta['fields'].keys()
246
+
247
+ def create_issue(
248
+ self,
249
+ project_key: str,
250
+ summary: str,
251
+ description: str,
252
+ issue_type: str,
253
+ assignee: Optional[str] = None,
254
+ custom_fields: Optional[dict] = None,
255
+ ) -> Optional[Issue]:
256
+ issue_fields = {
257
+ "project": {"key": project_key},
258
+ "issuetype": {"name": issue_type},
259
+ "summary": summary,
260
+ "description": description,
261
+ }
262
+ if assignee:
263
+ issue_fields["assignee"] = {"accountId": assignee} if self.server_type == ServerType.CLOUD else {"name": assignee}
264
+
265
+ if custom_fields:
266
+ issue_fields = issue_fields | custom_fields
267
+ self.validate_fields(project_key=project_key, issue_type=issue_type, fields=issue_fields)
268
+
269
+ try:
270
+ issue = self.jira_client.create_issue(fields=issue_fields)
271
+ self.logger.info(f"Issue {issue.key} created successfully.")
272
+ return issue
273
+ except JIRAError as err:
274
+ self.logger.error(f"JIRAError while creating issue: {err.text}")
275
+ return None
276
+ except Exception as err:
277
+ self.logger.exception(f"Unexpected error: {err}")
278
+ return None
279
+
280
+ def update_issue(
281
+ self,
282
+ issue_key: str,
283
+ fields_to_update: dict
284
+ ) -> Optional[Issue]:
285
+ try:
286
+ issue = self.jira_client.issue(issue_key)
287
+ self.validate_fields(
288
+ project_key=issue.fields.project.key,
289
+ issue_type=issue.fields.issuetype.name,
290
+ fields=fields_to_update
291
+ )
292
+
293
+ issue.update(fields=fields_to_update)
294
+ self.logger.info(f"Issue {issue.key} updated successfully.")
295
+ return issue
296
+ except JIRAError as err:
297
+ self.logger.error(f"JIRAError while updating issue: {err.text}")
298
+ return None
299
+ except Exception as err:
300
+ self.logger.exception(f"Unexpected error: {err}")
301
+ return None
302
+
303
+ def delete_issue(self, issue_key: str) -> bool:
304
+ try:
305
+ issue = self.get_issue_by_key(key=issue_key)
306
+ issue.delete()
307
+ self.logger.info(f"Issue {issue_key} deleted successfully.")
308
+ return True
309
+ except JIRAError as err:
310
+ self.logger.error(f"JIRAError while deleting issue: {err.text}")
311
+ return False
312
+ except Exception as err:
313
+ self.logger.exception(f"Unexpected error: {err}")
314
+ return False
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-jira-plus
3
+ Version: 0.1.0
4
+ Summary: Enhanced Python client for JIRA with better error handling, pagination, and metadata validation
5
+ Home-page: https://github.com/yourusername/python-jira-plus
6
+ Author: Avi Zaguri
7
+ Author-email:
8
+ Project-URL: Repository, https://github.com/aviz92/python-jira-plus
9
+ Keywords: jira,atlassian,api,client
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
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: jira>=3.1.1
19
+ Requires-Dist: retrying>=1.3.3
20
+ Requires-Dist: python-jira-plus>=0.1.4
21
+ Dynamic: author
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: project-url
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # Python JIRA Plus
34
+
35
+ [![PyPI version](https://badge.fury.io/py/python-jira-plus.svg)](https://badge.fury.io/py/python-jira-plus)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+
38
+ An enhanced Python client for JIRA that extends the functionality of the official `jira` package, providing better error handling, pagination, metadata validation, and more.
39
+
40
+ ---
41
+
42
+ ## Features
43
+ - ✅ Simplified connection to JIRA Cloud and On-Premise instances
44
+ - ✅ Robust error handling with automatic retries
45
+ - ✅ Built-in pagination for large result sets
46
+ - ✅ Field validation against JIRA metadata
47
+ - ✅ Enhanced issue creation, retrieval, and updating
48
+ - ✅ Support for allowed values validation
49
+
50
+ ---
51
+
52
+ ## Installation
53
+ ```bash
54
+ pip install python-jira-plus
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+ - Python 3.9+
61
+ - `jira` package
62
+ - `retrying` package
63
+ - `custom-python-logger` package
64
+
65
+ ---
66
+
67
+ ## Configuration
68
+ The package uses environment variables for authentication and configuration:
69
+
70
+ ```bash
71
+ # Required environment variables
72
+ JIRA_USER_NAME=your_jira_username
73
+ JIRA_TOKEN=your_jira_api_token
74
+ JIRA_BASE_URL=your-instance.atlassian.net # Only used if base_url is not provided to constructor
75
+ ```
76
+
77
+ ## Examples
78
+
79
+ ### Creating an Issue with Custom Fields
80
+ ```python
81
+ from python_jira_plus.jira_plus import JiraPlus
82
+
83
+ jira_client = JiraPlus()
84
+ issue = jira_client.create_issue(
85
+ project_key="PROJ",
86
+ summary="Implement new feature",
87
+ description="This feature will improve performance",
88
+ issue_type="Task",
89
+ custom_fields={
90
+ "priority": "Critical", # Priority
91
+ "customfield_10003": {"name": "Sprint 1"} # Sprint
92
+ }
93
+ )
94
+ ```
95
+
96
+ ### Searching for Issues
97
+ ```python
98
+ from python_jira_plus.jira_plus import JiraPlus
99
+
100
+ jira_client = JiraPlus()
101
+ issues = jira_client.get_objects_by_query(
102
+ query="project = PROJ AND status = 'In Progress' ORDER BY created DESC",
103
+ max_results=50,
104
+ specific_fields=["summary", "status", "assignee"],
105
+ json_result=False
106
+ )
107
+
108
+ for issue in issues:
109
+ print(f"{issue.key}: {issue.fields.summary} - {issue.fields.status.name}")
110
+ ```
111
+
112
+ ### Updating an Issue
113
+ ```python
114
+ from python_jira_plus.jira_plus import JiraPlus
115
+
116
+ jira_client = JiraPlus()
117
+ issue = jira_client.get_issue_by_key(key="PROJ-123", json_result=False)
118
+
119
+ fields_to_update = {
120
+ "summary": "Updated summary",
121
+ "description": "Updated description",
122
+ "customfield_10003": {"name": "Sprint 2"}, # Update Sprint
123
+ }
124
+ _ = jira_client.update_issue(
125
+ issue_key=issue.key,
126
+ fields_to_update=fields_to_update
127
+ )
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 🤝 Contributing
133
+ If you have a helpful tool, pattern, or improvement to suggest:
134
+ Fork the repo
135
+ Create a new branch
136
+ Submit a pull request
137
+ I welcome additions that promote clean, productive, and maintainable development.
138
+
139
+ ---
140
+
141
+ ## 🙏 Thanks
142
+ Thanks for exploring this repository!
143
+ Happy coding!
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,3 @@
1
+ jira>=3.1.1
2
+ retrying>=1.3.3
3
+ python-jira-plus>=0.1.4
@@ -0,0 +1,2 @@
1
+ python_jira_plus
2
+ usage_example
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
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.1.0",
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/yourusername/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
+ "python-jira-plus>=0.1.4",
31
+ ],
32
+ keywords="jira, atlassian, api, client",
33
+ )
File without changes
@@ -0,0 +1,26 @@
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()
@@ -0,0 +1,28 @@
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()
@@ -0,0 +1,24 @@
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()
@@ -0,0 +1,28 @@
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()