brynq-sdk-jira 2.0.0__tar.gz → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq_sdk_jira
3
- Version: 2.0.0
3
+ Version: 2.1.1
4
4
  Summary: JIRA wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -14,6 +14,7 @@ class Jira(BrynQ):
14
14
  "Content-Type": "application/json"
15
15
  }
16
16
  self.debug = debug
17
+ self.timeout = 3600
17
18
 
18
19
  def get_issues(self, jql_filter: str = None, jira_filter_id: int = None, get_extra_fields: list = None, expand_fields: list = None) -> pd.DataFrame:
19
20
  """
@@ -46,7 +47,7 @@ class Jira(BrynQ):
46
47
  url = f"{self.base_url}rest/api/3/search/jira/filter/{jira_filter_id}"
47
48
  else:
48
49
  url = f"{self.base_url}rest/api/3/search"
49
- response = requests.post(url=url, headers=self.headers, data=json.dumps(query))
50
+ response = requests.post(url=url, headers=self.headers, data=json.dumps(query), timeout=self.timeout)
50
51
  if response.status_code == 200:
51
52
  response_json = response.json()
52
53
  no_of_loops += 1
@@ -79,7 +80,7 @@ class Jira(BrynQ):
79
80
  }
80
81
  if self.debug:
81
82
  print(query)
82
- response = requests.get(f"{self.base_url}rest/api/3/project/search", headers=self.headers, params=query)
83
+ response = requests.get(f"{self.base_url}rest/api/3/project/search", headers=self.headers, params=query, timeout=self.timeout)
83
84
  if response.status_code == 200:
84
85
  response_json = response.json()
85
86
  response.raise_for_status()
@@ -103,7 +104,7 @@ class Jira(BrynQ):
103
104
  :return: A dataframe with the versions.
104
105
  """
105
106
  url = f"{self.base_url}rest/api/latest/project/{project_key}/versions"
106
- response = requests.get(url=url, headers=self.headers)
107
+ response = requests.get(url=url, headers=self.headers, timeout=self.timeout)
107
108
  if response.status_code == 200:
108
109
  response_json = response.json()
109
110
  df = pd.json_normalize(response_json)
@@ -111,4 +112,34 @@ class Jira(BrynQ):
111
112
  print(f"Received {len(df)} versions for project {project_key}")
112
113
  return df
113
114
  else:
114
- raise ConnectionError(f"Error getting versions from Jira with message: {response.status_code, response.text}")
115
+ raise ConnectionError(f"Error getting versions from Jira with message: {response.status_code, response.text}")
116
+
117
+ def get_users(self) -> pd.DataFrame:
118
+ """
119
+ This method retrieves users from Jira.
120
+ :return: a dataframe with users
121
+ """
122
+ start_at = 0
123
+ max_results = 50
124
+ all_users = []
125
+
126
+ while True:
127
+ response = requests.get(f"{self.base_url}rest/api/3/users/search?startAt={start_at}&maxResults={max_results}", headers=self.headers, timeout=self.timeout)
128
+ response.raise_for_status()
129
+ if response.status_code == 200:
130
+ users = response.json() # A list of user objects
131
+ all_users.extend(users) # Add users to the total list
132
+
133
+ # Stop if no more users are returned
134
+ if not users:
135
+ break
136
+
137
+ # Increment startAt for the next page
138
+ start_at += len(users)
139
+ else:
140
+ raise ConnectionError(f"Error getting users from Jira with message: {response.status_code, response.text}")
141
+ if self.debug:
142
+ print(f"Received {len(all_users)} jira users from Jira")
143
+
144
+ df = pd.json_normalize(all_users)
145
+ return df
@@ -0,0 +1,189 @@
1
+ import json
2
+ import requests
3
+ from itertools import islice
4
+ from brynq_sdk_brynq import BrynQ
5
+ from typing import Union, List
6
+
7
+
8
+ class Tempo(BrynQ):
9
+ def __init__(self, label: Union[str, List], debug=False):
10
+ super().__init__()
11
+ self.debug = debug
12
+ credentials = self.get_system_credential(system='tempo-timesheets', label=label)
13
+ self.headers = {
14
+ "Authorization": f"Bearer {credentials['api_token']}",
15
+ "Content-Type": "application/json"
16
+ }
17
+ if self.debug:
18
+ print(self.headers)
19
+ self.timeout = 3600
20
+
21
+ def get_tempo_hours(self, from_date: str = None, to_date: str = None, updated_from: str = None) -> json:
22
+ """
23
+ This function gets hours from Tempo for the specified time period
24
+
25
+ :param from_date: (Optional) string - retrieve results starting with this date
26
+ :param to_date: (Optional) string - retrieve results up to and including this date
27
+ :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")
28
+ :return: json response with results
29
+ """
30
+ total_response = []
31
+ got_all_results = False
32
+ no_of_loops = 0
33
+ parameters = {}
34
+ if from_date is not None:
35
+ parameters.update({"from": from_date})
36
+ if to_date is not None:
37
+ parameters.update({"to": to_date})
38
+ if updated_from is not None:
39
+ parameters.update({"updatedFrom": updated_from})
40
+
41
+ while not got_all_results:
42
+ loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
43
+ response = requests.get('https://api.tempo.io/4/worklogs', headers=self.headers, params=loop_parameters, timeout=self.timeout)
44
+ if response.status_code == 200:
45
+ response_json = response.json()
46
+ no_of_loops += 1
47
+ got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
48
+ total_response += response_json['results']
49
+ else:
50
+ raise ConnectionError(f"Error getting worklogs from Tempo: {response.status_code, response.text}")
51
+
52
+ if self.debug:
53
+ print(f"Received {len(total_response)} lines from Tempo")
54
+
55
+ return total_response
56
+
57
+ def get_tempo_teams(self, team_members: List[str] = None, name: str = None) -> json:
58
+ """
59
+ Fetches teams from the Tempo API in smaller batches to prevent long URLs if team_members is specified,
60
+ otherwise, retrieves a list of all existing Teams.
61
+
62
+ :param team_members: (Optional) List of Jira user account IDs to filter teams.
63
+ :param name: (Optional) Name of the team to filter teams.
64
+ :return: A json response containing team details.
65
+ """
66
+ total_response = []
67
+
68
+ # Split team members into smaller chunks (avoid long URLs)
69
+ team_member_chunks = self._chunk_list(team_members, 50) if team_members else [None]
70
+
71
+ for team_chunk in team_member_chunks:
72
+ parameters = {"limit": 1000, "offset": 0}
73
+ if team_chunk:
74
+ parameters["teamMembers"] = ",".join(team_chunk) # Send fewer team members at a time
75
+ if name:
76
+ parameters["name"] = name
77
+ got_all_results = False
78
+ no_of_loops = 0
79
+
80
+ while not got_all_results:
81
+ parameters["offset"] = 1000 * no_of_loops
82
+ response = requests.get('https://api.tempo.io/4/teams', headers=self.headers, params=parameters, timeout=self.timeout)
83
+ if response.status_code == 200:
84
+ response_json = response.json()
85
+ total_response.extend(response_json["results"])
86
+ got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
87
+ no_of_loops += 1
88
+ else:
89
+ raise ConnectionError(f"Error getting teams from Tempo: {response.status_code}, {response.text}")
90
+
91
+ if self.debug:
92
+ print(f"Received {len(total_response)} teams from Tempo")
93
+ return total_response
94
+
95
+ def get_tempo_team_members(self, team_ids: List[int]) -> json:
96
+ """
97
+ Fetches members of multiple teams from the Tempo API iteratively.
98
+
99
+ :param team_ids: List of Tempo team IDs to retrieve members from.
100
+ :return: A json response containing team members' details.
101
+ """
102
+ total_response = []
103
+
104
+ for team_id in team_ids:
105
+ got_all_results = False
106
+ no_of_loops = 0
107
+
108
+ while not got_all_results:
109
+ parameters = {"limit": 1000, "offset": 1000 * no_of_loops}
110
+ response = requests.get(f'https://api.tempo.io/4/teams/{team_id}/members', headers=self.headers, params=parameters, timeout=self.timeout)
111
+ if response.status_code == 200:
112
+ response_json = response.json()
113
+ total_response.extend(response_json["results"])
114
+ got_all_results = False if int(response_json.get('metadata', {}).get('count', 0)) == 1000 else True
115
+ no_of_loops += 1
116
+ else:
117
+ raise ConnectionError(f"Error getting team members from Tempo: {response.status_code}, {response.text}")
118
+
119
+ if self.debug:
120
+ print(f"Received {len(total_response)} team members from Tempo")
121
+ return total_response
122
+
123
+ def get_accounts(self) -> json:
124
+ """
125
+ Fetches account details from the Tempo API in batches to handle large datasets.
126
+
127
+ :return: A json object containing account details.
128
+ """
129
+ total_response = []
130
+ got_all_results = False
131
+ no_of_loops = 0
132
+ parameters = {}
133
+
134
+ while not got_all_results:
135
+ loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
136
+ response = requests.get('https://api.tempo.io/4/accounts', headers=self.headers, params=loop_parameters, timeout=self.timeout)
137
+ if response.status_code == 200:
138
+ response_json = response.json()
139
+ no_of_loops += 1
140
+ got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
141
+ total_response += response_json['results']
142
+ else:
143
+ raise ConnectionError(f"Error getting accounts from Tempo: {response.status_code}, {response.text}")
144
+
145
+ if self.debug:
146
+ print(f"Received {len(total_response)} accounts from Tempo")
147
+ return total_response
148
+
149
+ def get_worklog_accounts(self, account_key: str, from_date: str = None, to_date: str = None, updated_from: str = None) -> json:
150
+ """
151
+ Fetches worklog data for a given account key from the Tempo API.
152
+
153
+ :param account_key: (Required) string - The account key for which worklog data is required.
154
+ :param from_date: (Optional) string - retrieve results starting with this date
155
+ :param to_date: (Optional) string - retrieve results up to and including this date
156
+ :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")
157
+ :return: A json containing worklog details.
158
+ """
159
+ total_response = []
160
+ got_all_results = False
161
+ no_of_loops = 0
162
+ parameters = {}
163
+ if from_date is not None:
164
+ parameters.update({"from": from_date})
165
+ if to_date is not None:
166
+ parameters.update({"to": to_date})
167
+ if updated_from is not None:
168
+ parameters.update({"updatedFrom": updated_from})
169
+
170
+ while not got_all_results:
171
+ loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
172
+ response = requests.get(f"https://api.tempo.io/4/worklogs/account/{account_key}", headers=self.headers, params=loop_parameters, timeout=self.timeout)
173
+ if response.status_code == 200:
174
+ response_json = response.json()
175
+ total_response.extend(response_json.get("results", []))
176
+ got_all_results = False if int(response_json.get('metadata', {}).get('count', 0)) == 1000 else True
177
+ no_of_loops += 1
178
+ else:
179
+ raise ConnectionError(f"Failed to fetch data for account key {account_key}: {response.status_code}, {response.text}")
180
+
181
+ if self.debug:
182
+ print(f"Received {len(total_response)} worklogs for account key {account_key}")
183
+
184
+ return total_response
185
+
186
+ def _chunk_list(self, data_list, chunk_size):
187
+ """Splits a list into chunks of `chunk_size`."""
188
+ it = iter(data_list)
189
+ return iter(lambda: list(islice(it, chunk_size)), [])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq-sdk-jira
3
- Version: 2.0.0
3
+ Version: 2.1.1
4
4
  Summary: JIRA wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -2,7 +2,7 @@ from setuptools import setup, find_namespace_packages
2
2
 
3
3
  setup(
4
4
  name='brynq_sdk_jira',
5
- version='2.0.0',
5
+ version='2.1.1',
6
6
  description='JIRA wrapper from BrynQ',
7
7
  long_description='JIRA wrapper from BrynQ',
8
8
  author='BrynQ',
@@ -1,50 +0,0 @@
1
- import json
2
- import requests
3
- from typing import Union, List
4
- from brynq_sdk_brynq import BrynQ
5
-
6
- class Tempo(BrynQ):
7
- def __init__(self, label: Union[str, List], debug=False):
8
- super().__init__()
9
- self.debug = debug
10
- credentials = self.get_system_credential(system='tempo-timesheets', label=label)
11
- self.headers = {
12
- "Authorization": f"Bearer {credentials['api_token']}",
13
- "Content-Type": "application/json"
14
- }
15
- if self.debug:
16
- print(self.headers)
17
-
18
- def get_tempo_hours(self, from_date: str = None, to_date: str = None, updated_from: str = None) -> json:
19
- """
20
- This function gets hours from Tempo for max 8 backs week
21
- :param from_date:
22
- :param to_date:
23
- :return: json response with results
24
- """
25
- total_response = []
26
- got_all_results = False
27
- no_of_loops = 0
28
- parameters = {}
29
- if from_date is not None:
30
- parameters.update({"from": from_date})
31
- if to_date is not None:
32
- parameters.update({"to": to_date})
33
- if updated_from is not None:
34
- parameters.update({"updatedFrom": updated_from})
35
-
36
- while not got_all_results:
37
- loop_parameters = parameters | {"limit": 1000, "offset": 1000 * no_of_loops}
38
- response = requests.get('https://api.tempo.io/4/worklogs', headers=self.headers, params=loop_parameters)
39
- if response.status_code == 200:
40
- response_json = response.json()
41
- no_of_loops += 1
42
- got_all_results = False if int(response_json['metadata']['count']) == 1000 else True
43
- total_response += response_json['results']
44
- else:
45
- raise ConnectionError(f"Error getting worklogs from Tempo: {response.status_code, response.text}")
46
-
47
- if self.debug:
48
- print(f"Received {len(total_response)} lines from Tempo")
49
-
50
- return total_response
File without changes