brynq-sdk-jira 3.0.4__py3-none-any.whl
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.
brynq_sdk_jira/jira.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Union, List, Literal, Optional
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import requests
|
|
5
|
+
from brynq_sdk_brynq import BrynQ
|
|
6
|
+
|
|
7
|
+
class Jira(BrynQ):
|
|
8
|
+
def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug=False):
|
|
9
|
+
super().__init__()
|
|
10
|
+
credentials = self.interfaces.credentials.get(system="jira", system_type=system_type)
|
|
11
|
+
credentials = credentials.get('data')
|
|
12
|
+
self.base_url = credentials['base_url']
|
|
13
|
+
self.headers = {
|
|
14
|
+
"Authorization": f"Basic {credentials['access_token']}",
|
|
15
|
+
"Content-Type": "application/json"
|
|
16
|
+
}
|
|
17
|
+
self.debug = debug
|
|
18
|
+
self.timeout = 3600
|
|
19
|
+
|
|
20
|
+
def get_issues(self, jql_filter: str = None, jira_filter_id: int = None, get_extra_fields: list = None, expand_fields: list = None) -> pd.DataFrame:
|
|
21
|
+
"""
|
|
22
|
+
This method retrieves issues from Jira.
|
|
23
|
+
:param jql_filter: optional filter in jql format
|
|
24
|
+
:param jira_filter_id: optional filter id of predefined filter in jira
|
|
25
|
+
:param get_extra_fields: an optional list of extra fields to retrieve
|
|
26
|
+
:param expand_fields: an optional list of fields to expand
|
|
27
|
+
:return: dataframe with issues
|
|
28
|
+
"""
|
|
29
|
+
if jira_filter_id is not None:
|
|
30
|
+
raise ValueError("Jira filter id is no longer supported, use jql_filter instead")
|
|
31
|
+
|
|
32
|
+
# Use new JQL search endpoint
|
|
33
|
+
url = f"{self.base_url}rest/api/3/search/jql"
|
|
34
|
+
|
|
35
|
+
all_issues = []
|
|
36
|
+
next_page_token = None
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
payload = {
|
|
40
|
+
"maxResults": 100,
|
|
41
|
+
"fields": ["summary", "issuetype", "timetracking", "timespent", "description", "assignee", "project"]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if jql_filter:
|
|
45
|
+
payload["jql"] = jql_filter
|
|
46
|
+
|
|
47
|
+
if get_extra_fields:
|
|
48
|
+
payload["fields"].extend(get_extra_fields)
|
|
49
|
+
|
|
50
|
+
if expand_fields:
|
|
51
|
+
# Convert list to comma-delimited string
|
|
52
|
+
payload["expand"] = ",".join(expand_fields)
|
|
53
|
+
|
|
54
|
+
if next_page_token:
|
|
55
|
+
payload["nextPageToken"] = next_page_token
|
|
56
|
+
|
|
57
|
+
if self.debug:
|
|
58
|
+
print(f"Payload: {payload}")
|
|
59
|
+
|
|
60
|
+
response = requests.post(
|
|
61
|
+
url=url,
|
|
62
|
+
headers=self.headers,
|
|
63
|
+
data=json.dumps(payload),
|
|
64
|
+
timeout=self.timeout
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if response.status_code == 200:
|
|
68
|
+
response_json = response.json()
|
|
69
|
+
all_issues.extend(response_json.get("issues", []))
|
|
70
|
+
|
|
71
|
+
# Check for next page
|
|
72
|
+
if "nextPageToken" in response_json:
|
|
73
|
+
next_page_token = response_json["nextPageToken"]
|
|
74
|
+
else:
|
|
75
|
+
break
|
|
76
|
+
else:
|
|
77
|
+
raise ConnectionError(f"Error getting issues from Jira with message: {response.status_code, response.text}")
|
|
78
|
+
|
|
79
|
+
if self.debug:
|
|
80
|
+
print(f"Received {len(all_issues)} issues from Jira")
|
|
81
|
+
|
|
82
|
+
df = pd.json_normalize(all_issues)
|
|
83
|
+
return df
|
|
84
|
+
|
|
85
|
+
def get_projects(self) -> pd.DataFrame:
|
|
86
|
+
"""
|
|
87
|
+
This method retrieves projects from Jira.
|
|
88
|
+
:return: a dataframe with projects
|
|
89
|
+
"""
|
|
90
|
+
total_response = []
|
|
91
|
+
got_all_results = False
|
|
92
|
+
no_of_loops = 0
|
|
93
|
+
|
|
94
|
+
while not got_all_results:
|
|
95
|
+
query = {
|
|
96
|
+
'startAt': f'{50 * no_of_loops}',
|
|
97
|
+
'maxResults': '50',
|
|
98
|
+
'expand': 'description'
|
|
99
|
+
}
|
|
100
|
+
if self.debug:
|
|
101
|
+
print(query)
|
|
102
|
+
response = requests.get(f"{self.base_url}rest/api/3/project/search", headers=self.headers, params=query, timeout=self.timeout)
|
|
103
|
+
if response.status_code == 200:
|
|
104
|
+
response_json = response.json()
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
no_of_loops += 1
|
|
107
|
+
got_all_results = False if len(response_json['values']) == 50 else True
|
|
108
|
+
total_response += response_json['values']
|
|
109
|
+
else:
|
|
110
|
+
raise ConnectionError(f"Error getting projects from Jira with message: {response.status_code, response.text}")
|
|
111
|
+
|
|
112
|
+
if self.debug:
|
|
113
|
+
print(f"Received {len(total_response)} projects from Jira")
|
|
114
|
+
|
|
115
|
+
df = pd.json_normalize(total_response)
|
|
116
|
+
|
|
117
|
+
return df
|
|
118
|
+
|
|
119
|
+
def get_versions(self, project_key: str) -> pd.DataFrame:
|
|
120
|
+
"""
|
|
121
|
+
This method retrieves versions for a given project from Jira.
|
|
122
|
+
:param project_key: The key of the project for which versions are to be retrieved.
|
|
123
|
+
:return: A dataframe with the versions.
|
|
124
|
+
"""
|
|
125
|
+
url = f"{self.base_url}rest/api/latest/project/{project_key}/versions"
|
|
126
|
+
response = requests.get(url=url, headers=self.headers, timeout=self.timeout)
|
|
127
|
+
if response.status_code == 200:
|
|
128
|
+
response_json = response.json()
|
|
129
|
+
df = pd.json_normalize(response_json)
|
|
130
|
+
if self.debug:
|
|
131
|
+
print(f"Received {len(df)} versions for project {project_key}")
|
|
132
|
+
return df
|
|
133
|
+
else:
|
|
134
|
+
raise ConnectionError(f"Error getting versions from Jira with message: {response.status_code, response.text}")
|
|
135
|
+
|
|
136
|
+
def get_users(self) -> pd.DataFrame:
|
|
137
|
+
"""
|
|
138
|
+
This method retrieves users from Jira.
|
|
139
|
+
:return: a dataframe with users
|
|
140
|
+
"""
|
|
141
|
+
start_at = 0
|
|
142
|
+
max_results = 50
|
|
143
|
+
all_users = []
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
response = requests.get(f"{self.base_url}rest/api/3/users/search?startAt={start_at}&maxResults={max_results}", headers=self.headers, timeout=self.timeout)
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
if response.status_code == 200:
|
|
149
|
+
users = response.json() # A list of user objects
|
|
150
|
+
all_users.extend(users) # Add users to the total list
|
|
151
|
+
|
|
152
|
+
# Stop if no more users are returned
|
|
153
|
+
if not users:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# Increment startAt for the next page
|
|
157
|
+
start_at += len(users)
|
|
158
|
+
else:
|
|
159
|
+
raise ConnectionError(f"Error getting users from Jira with message: {response.status_code, response.text}")
|
|
160
|
+
if self.debug:
|
|
161
|
+
print(f"Received {len(all_users)} jira users from Jira")
|
|
162
|
+
|
|
163
|
+
df = pd.json_normalize(all_users)
|
|
164
|
+
return df
|
|
165
|
+
|
|
166
|
+
def update_issue(self, issue, fields : dict):
|
|
167
|
+
try:
|
|
168
|
+
url = f"{self.base_url}rest/api/3/issue/{issue}"
|
|
169
|
+
payload = {
|
|
170
|
+
"fields" : fields
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resp = requests.put(
|
|
174
|
+
url,
|
|
175
|
+
json= payload,
|
|
176
|
+
headers= self.headers,
|
|
177
|
+
timeout=60
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return resp
|
|
181
|
+
except Exception as e:
|
|
182
|
+
message = "Error updating issue"
|
|
183
|
+
return message
|
brynq_sdk_jira/tempo.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from itertools import islice
|
|
4
|
+
from brynq_sdk_brynq import BrynQ
|
|
5
|
+
from typing import Union, List, Literal, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Tempo(BrynQ):
|
|
9
|
+
def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug=False):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.debug = debug
|
|
12
|
+
credentials = self.interfaces.credentials.get(system="jira", system_type=system_type)
|
|
13
|
+
credentials = credentials.get('data')
|
|
14
|
+
self.headers = {
|
|
15
|
+
"Authorization": f"Bearer {credentials['api_token']}",
|
|
16
|
+
"Content-Type": "application/json"
|
|
17
|
+
}
|
|
18
|
+
if self.debug:
|
|
19
|
+
print(self.headers)
|
|
20
|
+
self.timeout = 3600
|
|
21
|
+
|
|
22
|
+
def get_tempo_hours(self, from_date: str = None, to_date: str = None, updated_from: str = None) -> json:
|
|
23
|
+
"""
|
|
24
|
+
This function gets hours from Tempo for the specified time period
|
|
25
|
+
|
|
26
|
+
:param from_date: (Optional) string - retrieve results starting with this date
|
|
27
|
+
:param to_date: (Optional) string - retrieve results up to and including this date
|
|
28
|
+
:param updated_from: (Optional) string <yyyy-MM-dd['T'HH:mm:ss]['Z']> - retrieve results that have been updated from this date(e.g "2023-11-16") or date time (e.g "2023-11-06T16:48:59Z")
|
|
29
|
+
:return: json response with results
|
|
30
|
+
"""
|
|
31
|
+
total_response = []
|
|
32
|
+
got_all_results = False
|
|
33
|
+
no_of_loops = 0
|
|
34
|
+
parameters = {}
|
|
35
|
+
if from_date is not None:
|
|
36
|
+
parameters.update({"from": from_date})
|
|
37
|
+
if to_date is not None:
|
|
38
|
+
parameters.update({"to": to_date})
|
|
39
|
+
if updated_from is not None:
|
|
40
|
+
parameters.update({"updatedFrom": updated_from})
|
|
41
|
+
|
|
42
|
+
while not got_all_results:
|
|
43
|
+
loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
|
|
44
|
+
response = requests.get('https://api.tempo.io/4/worklogs', headers=self.headers, params=loop_parameters, timeout=self.timeout)
|
|
45
|
+
if response.status_code == 200:
|
|
46
|
+
response_json = response.json()
|
|
47
|
+
no_of_loops += 1
|
|
48
|
+
got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
|
|
49
|
+
total_response += response_json['results']
|
|
50
|
+
else:
|
|
51
|
+
raise ConnectionError(f"Error getting worklogs from Tempo: {response.status_code, response.text}")
|
|
52
|
+
|
|
53
|
+
if self.debug:
|
|
54
|
+
print(f"Received {len(total_response)} lines from Tempo")
|
|
55
|
+
|
|
56
|
+
return total_response
|
|
57
|
+
|
|
58
|
+
def get_tempo_timesheet_approvals(self, from_date: str, to_date: str, team_id: int = 19) -> json:
|
|
59
|
+
"""
|
|
60
|
+
This function retrieves approved timesheet approvals for a given team in Tempo
|
|
61
|
+
over the specified date range.
|
|
62
|
+
|
|
63
|
+
:param from_date: string <yyyy-MM-dd> - retrieve results starting with this date
|
|
64
|
+
:param to_date: string <yyyy-MM-dd> - retrieve results up to and including this date
|
|
65
|
+
:param team_id: int (default 19) - Tempo team ID whose approvals are retrieved
|
|
66
|
+
:return: json response with results
|
|
67
|
+
"""
|
|
68
|
+
total_response = []
|
|
69
|
+
got_all_results = False
|
|
70
|
+
no_of_loops = 0
|
|
71
|
+
|
|
72
|
+
parameters = {
|
|
73
|
+
"from": from_date,
|
|
74
|
+
"to": to_date,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
while not got_all_results:
|
|
78
|
+
loop_parameters = parameters | {"limit": 50, "offset": 50 * no_of_loops}
|
|
79
|
+
url = f"https://api.tempo.io/4/timesheet-approvals/team/{team_id}"
|
|
80
|
+
response = requests.get(
|
|
81
|
+
url,
|
|
82
|
+
headers=self.headers,
|
|
83
|
+
params=loop_parameters,
|
|
84
|
+
timeout=self.timeout
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if response.status_code == 200:
|
|
88
|
+
response_json = response.json()
|
|
89
|
+
no_of_loops += 1
|
|
90
|
+
got_all_results = False if int(response_json['metadata']['count']) == 50 else True
|
|
91
|
+
total_response += response_json['results']
|
|
92
|
+
else:
|
|
93
|
+
raise ConnectionError(
|
|
94
|
+
f"Error getting timesheet approvals from Tempo: {response.status_code, response.text}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if self.debug:
|
|
98
|
+
print(f"Received {len(total_response)} timesheet approvals from Tempo")
|
|
99
|
+
|
|
100
|
+
return total_response
|
|
101
|
+
|
|
102
|
+
def call_api(self, url: str, limit: int = 50) -> list[dict]:
|
|
103
|
+
"""
|
|
104
|
+
Calls the Tempo API and retrieves all paginated results for a given endpoint.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
url (str): The API endpoint URL to call.
|
|
108
|
+
limit (int): Max results to fetch per request (default 50).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
list[dict]: A list of all results across all pages.
|
|
112
|
+
"""
|
|
113
|
+
all_results = []
|
|
114
|
+
offset = 0
|
|
115
|
+
while True:
|
|
116
|
+
querystring = {"limit": str(limit), "offset": str(offset)}
|
|
117
|
+
response = requests.get(url, headers=self.headers, params=querystring)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
data = response.json()
|
|
120
|
+
|
|
121
|
+
# append results
|
|
122
|
+
results = data.get("results", [])
|
|
123
|
+
all_results.extend(results)
|
|
124
|
+
|
|
125
|
+
# pagination check
|
|
126
|
+
count = data.get("metadata", {}).get("count", 0)
|
|
127
|
+
if count < limit:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
offset += limit
|
|
131
|
+
|
|
132
|
+
return all_results
|
|
133
|
+
|
|
134
|
+
def get_tempo_teams(self, team_members: List[str] = None, name: str = None) -> json:
|
|
135
|
+
"""
|
|
136
|
+
Fetches teams from the Tempo API in smaller batches to prevent long URLs if team_members is specified,
|
|
137
|
+
otherwise, retrieves a list of all existing Teams.
|
|
138
|
+
|
|
139
|
+
:param team_members: (Optional) List of Jira user account IDs to filter teams.
|
|
140
|
+
:param name: (Optional) Name of the team to filter teams.
|
|
141
|
+
:return: A json response containing team details.
|
|
142
|
+
"""
|
|
143
|
+
total_response = []
|
|
144
|
+
|
|
145
|
+
# Split team members into smaller chunks (avoid long URLs)
|
|
146
|
+
team_member_chunks = self._chunk_list(team_members, 50) if team_members else [None]
|
|
147
|
+
|
|
148
|
+
for team_chunk in team_member_chunks:
|
|
149
|
+
parameters = {"limit": 1000, "offset": 0}
|
|
150
|
+
if team_chunk:
|
|
151
|
+
parameters["teamMembers"] = ",".join(team_chunk) # Send fewer team members at a time
|
|
152
|
+
if name:
|
|
153
|
+
parameters["name"] = name
|
|
154
|
+
got_all_results = False
|
|
155
|
+
no_of_loops = 0
|
|
156
|
+
|
|
157
|
+
while not got_all_results:
|
|
158
|
+
parameters["offset"] = 1000 * no_of_loops
|
|
159
|
+
response = requests.get('https://api.tempo.io/4/teams', headers=self.headers, params=parameters, timeout=self.timeout)
|
|
160
|
+
if response.status_code == 200:
|
|
161
|
+
response_json = response.json()
|
|
162
|
+
total_response.extend(response_json["results"])
|
|
163
|
+
got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
|
|
164
|
+
no_of_loops += 1
|
|
165
|
+
else:
|
|
166
|
+
raise ConnectionError(f"Error getting teams from Tempo: {response.status_code}, {response.text}")
|
|
167
|
+
|
|
168
|
+
if self.debug:
|
|
169
|
+
print(f"Received {len(total_response)} teams from Tempo")
|
|
170
|
+
return total_response
|
|
171
|
+
|
|
172
|
+
def get_tempo_team_members(self, team_ids: List[int]) -> json:
|
|
173
|
+
"""
|
|
174
|
+
Fetches members of multiple teams from the Tempo API iteratively.
|
|
175
|
+
|
|
176
|
+
:param team_ids: List of Tempo team IDs to retrieve members from.
|
|
177
|
+
:return: A json response containing team members' details.
|
|
178
|
+
"""
|
|
179
|
+
total_response = []
|
|
180
|
+
|
|
181
|
+
for team_id in team_ids:
|
|
182
|
+
got_all_results = False
|
|
183
|
+
no_of_loops = 0
|
|
184
|
+
|
|
185
|
+
while not got_all_results:
|
|
186
|
+
parameters = {"limit": 1000, "offset": 1000 * no_of_loops}
|
|
187
|
+
response = requests.get(f'https://api.tempo.io/4/teams/{team_id}/members', headers=self.headers, params=parameters, timeout=self.timeout)
|
|
188
|
+
if response.status_code == 200:
|
|
189
|
+
response_json = response.json()
|
|
190
|
+
total_response.extend(response_json["results"])
|
|
191
|
+
got_all_results = False if int(response_json.get('metadata', {}).get('count', 0)) == 1000 else True
|
|
192
|
+
no_of_loops += 1
|
|
193
|
+
else:
|
|
194
|
+
raise ConnectionError(f"Error getting team members from Tempo: {response.status_code}, {response.text}")
|
|
195
|
+
|
|
196
|
+
if self.debug:
|
|
197
|
+
print(f"Received {len(total_response)} team members from Tempo")
|
|
198
|
+
return total_response
|
|
199
|
+
|
|
200
|
+
def get_accounts(self) -> json:
|
|
201
|
+
"""
|
|
202
|
+
Fetches account details from the Tempo API in batches to handle large datasets.
|
|
203
|
+
|
|
204
|
+
:return: A json object containing account details.
|
|
205
|
+
"""
|
|
206
|
+
total_response = []
|
|
207
|
+
got_all_results = False
|
|
208
|
+
no_of_loops = 0
|
|
209
|
+
parameters = {}
|
|
210
|
+
|
|
211
|
+
while not got_all_results:
|
|
212
|
+
loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
|
|
213
|
+
response = requests.get('https://api.tempo.io/4/accounts', headers=self.headers, params=loop_parameters, timeout=self.timeout)
|
|
214
|
+
if response.status_code == 200:
|
|
215
|
+
response_json = response.json()
|
|
216
|
+
no_of_loops += 1
|
|
217
|
+
got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
|
|
218
|
+
total_response += response_json['results']
|
|
219
|
+
else:
|
|
220
|
+
raise ConnectionError(f"Error getting accounts from Tempo: {response.status_code}, {response.text}")
|
|
221
|
+
|
|
222
|
+
if self.debug:
|
|
223
|
+
print(f"Received {len(total_response)} accounts from Tempo")
|
|
224
|
+
return total_response
|
|
225
|
+
|
|
226
|
+
def get_worklog_accounts(self, account_key: str, from_date: str = None, to_date: str = None, updated_from: str = None) -> json:
|
|
227
|
+
"""
|
|
228
|
+
Fetches worklog data for a given account key from the Tempo API.
|
|
229
|
+
|
|
230
|
+
:param account_key: (Required) string - The account key for which worklog data is required.
|
|
231
|
+
:param from_date: (Optional) string - retrieve results starting with this date
|
|
232
|
+
:param to_date: (Optional) string - retrieve results up to and including this date
|
|
233
|
+
:param updated_from: (Optional) string <yyyy-MM-dd['T'HH:mm:ss]['Z']> - retrieve results that have been updated from this date(e.g "2023-11-16") or date time (e.g "2023-11-06T16:48:59Z")
|
|
234
|
+
:return: A json containing worklog details.
|
|
235
|
+
"""
|
|
236
|
+
total_response = []
|
|
237
|
+
got_all_results = False
|
|
238
|
+
no_of_loops = 0
|
|
239
|
+
parameters = {}
|
|
240
|
+
if from_date is not None:
|
|
241
|
+
parameters.update({"from": from_date})
|
|
242
|
+
if to_date is not None:
|
|
243
|
+
parameters.update({"to": to_date})
|
|
244
|
+
if updated_from is not None:
|
|
245
|
+
parameters.update({"updatedFrom": updated_from})
|
|
246
|
+
|
|
247
|
+
while not got_all_results:
|
|
248
|
+
loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
|
|
249
|
+
response = requests.get(f"https://api.tempo.io/4/worklogs/account/{account_key}", headers=self.headers, params=loop_parameters, timeout=self.timeout)
|
|
250
|
+
if response.status_code == 200:
|
|
251
|
+
response_json = response.json()
|
|
252
|
+
total_response.extend(response_json.get("results", []))
|
|
253
|
+
got_all_results = False if int(response_json.get('metadata', {}).get('count', 0)) == 1000 else True
|
|
254
|
+
no_of_loops += 1
|
|
255
|
+
else:
|
|
256
|
+
raise ConnectionError(f"Failed to fetch data for account key {account_key}: {response.status_code}, {response.text}")
|
|
257
|
+
|
|
258
|
+
if self.debug:
|
|
259
|
+
print(f"Received {len(total_response)} worklogs for account key {account_key}")
|
|
260
|
+
|
|
261
|
+
return total_response
|
|
262
|
+
|
|
263
|
+
def update_worklog(self, worklog_id: Union[str, int], data: Union[str, dict]) -> requests.Response:
|
|
264
|
+
"""
|
|
265
|
+
Updates a Tempo worklog by ID.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
worklog_id (str | int): ID of the worklog to update.
|
|
269
|
+
data (str | dict): The payload to send in the update request (JSON string or dict).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
requests.Response: The HTTP response object from Tempo API.
|
|
273
|
+
"""
|
|
274
|
+
url = f"https://api.tempo.io/4/worklogs/{worklog_id}"
|
|
275
|
+
|
|
276
|
+
# Ensure we send valid JSON
|
|
277
|
+
if isinstance(data, dict):
|
|
278
|
+
payload = json.dumps(data)
|
|
279
|
+
else:
|
|
280
|
+
payload = data
|
|
281
|
+
|
|
282
|
+
response = requests.put(url, headers=self.headers, data=payload, timeout=self.timeout)
|
|
283
|
+
return response
|
|
284
|
+
|
|
285
|
+
def _chunk_list(self, data_list, chunk_size):
|
|
286
|
+
"""Splits a list into chunks of `chunk_size`."""
|
|
287
|
+
it = iter(data_list)
|
|
288
|
+
return iter(lambda: list(islice(it, chunk_size)), [])
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brynq_sdk_jira
|
|
3
|
+
Version: 3.0.4
|
|
4
|
+
Summary: JIRA wrapper from BrynQ
|
|
5
|
+
Author: BrynQ
|
|
6
|
+
Author-email: support@brynq.com
|
|
7
|
+
License: BrynQ License
|
|
8
|
+
Requires-Dist: brynq-sdk-brynq<5,>=4
|
|
9
|
+
Requires-Dist: pandas<3,>=1
|
|
10
|
+
Requires-Dist: requests<=3,>=2
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: license
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: summary
|
|
17
|
+
|
|
18
|
+
JIRA wrapper from BrynQ
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
brynq_sdk_jira/__init__.py,sha256=sw18oQvBYeGrqYrTXfMANQSE0B3BJ2TFetEy06h8wAU,47
|
|
2
|
+
brynq_sdk_jira/jira.py,sha256=KH2bBOWkWYpDniIWbRc31n8J62j1Hx-5amgKWItYLPM,6797
|
|
3
|
+
brynq_sdk_jira/tempo.py,sha256=mcRXfGHbxxS9cVlEnflzCv8RXyKY4Rj9L_ErdLkR5Yo,12505
|
|
4
|
+
brynq_sdk_jira-3.0.4.dist-info/METADATA,sha256=kfHATSRlPw9EXWzsv7GGVt7UscXV8Lx5T99OQfox_YY,397
|
|
5
|
+
brynq_sdk_jira-3.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
brynq_sdk_jira-3.0.4.dist-info/top_level.txt,sha256=e4NQdxchgAtFyXeuPqqN4Mbx2BbrUk2Cf7vNKRQrcys,15
|
|
7
|
+
brynq_sdk_jira-3.0.4.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brynq_sdk_jira
|