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.
- python_jira_plus-0.1.0/LICENSE +21 -0
- python_jira_plus-0.1.0/MANIFEST.in +2 -0
- python_jira_plus-0.1.0/PKG-INFO +143 -0
- python_jira_plus-0.1.0/README.md +111 -0
- python_jira_plus-0.1.0/pyproject.toml +11 -0
- python_jira_plus-0.1.0/python_jira_plus/__init__.py +35 -0
- python_jira_plus-0.1.0/python_jira_plus/describe_allowed_value.py +38 -0
- python_jira_plus-0.1.0/python_jira_plus/jira_plus.py +314 -0
- python_jira_plus-0.1.0/python_jira_plus.egg-info/PKG-INFO +143 -0
- python_jira_plus-0.1.0/python_jira_plus.egg-info/SOURCES.txt +18 -0
- python_jira_plus-0.1.0/python_jira_plus.egg-info/dependency_links.txt +1 -0
- python_jira_plus-0.1.0/python_jira_plus.egg-info/requires.txt +3 -0
- python_jira_plus-0.1.0/python_jira_plus.egg-info/top_level.txt +2 -0
- python_jira_plus-0.1.0/setup.cfg +4 -0
- python_jira_plus-0.1.0/setup.py +33 -0
- python_jira_plus-0.1.0/usage_example/__init__.py +0 -0
- python_jira_plus-0.1.0/usage_example/create_jira_issue.py +26 -0
- python_jira_plus-0.1.0/usage_example/featch_jira_issues.py +28 -0
- python_jira_plus-0.1.0/usage_example/get_jira_issue.py +24 -0
- python_jira_plus-0.1.0/usage_example/update_jira_issue.py +28 -0
|
@@ -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,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
|
+
[](https://badge.fury.io/py/python-jira-plus)
|
|
36
|
+
[](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
|
+
[](https://badge.fury.io/py/python-jira-plus)
|
|
4
|
+
[](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,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
|
+
[](https://badge.fury.io/py/python-jira-plus)
|
|
36
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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()
|