-
Notifications
You must be signed in to change notification settings - Fork 0
/
ximc.py
308 lines (266 loc) · 10.3 KB
/
ximc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
"""
File with class to work with Ximc Redmine.
"""
import re
import time
from typing import Callable, Dict, Iterable, List, Optional, Tuple
import requests
from bs4 import BeautifulSoup
from redminelib import Redmine
from redminelib.exceptions import ForbiddenError
from redminelib.resources.standard import Project, User
import utils as ut
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99.0.4844.82 Safari/537.36"}
MAX_ATTEMPTS_NUMBER = 5
def check_auth(func: Callable):
"""
Decorator checks authentication of user to Redmine.
:param func: decorated method.
"""
def wrapper(self, *args):
"""
:param self: object of class;
:param args: arguments for method.
"""
if self.user is None:
print("User is not logged in to https://ximc.ru")
return
return func(self, *args)
return wrapper
class XimcRedmine:
"""
Class to work with ximc Redmine.
"""
def __init__(self, username: str, password: str):
"""
:param username: username in ximc Redmine;
:param password: password to ximc Redmine.
"""
self._all_projects = None
self._filters: list = []
self._password: str = password
self._projects: list = []
self._redmine: Redmine = Redmine("https://ximc.ru", username=username, password=password)
self._totals_options: dict = {}
self._username: str = username
self.user: User = None
def _find_project_id(self, project_name: str) -> Optional[str]:
"""
Method searches identifier of project with given name.
:param project_name: project name.
:return: project identifier.
"""
projects = self._redmine.project.all()
if isinstance(projects, Iterable):
for project in projects:
if project.name == project_name:
return project.id
return None
def _get_user_id(self, username: str) -> Optional[int]:
"""
Method returns ID of user who works in given project.
:param username: username.
:return: user ID.
"""
def get_user_from_memberships(memberships) -> Optional[int]:
for membership in memberships:
user = getattr(membership, "user", None)
if user is not None and user.name == username:
return user.id
return None
if self._projects:
for project_id, _ in self._projects:
memberships = self._redmine.project_membership.filter(project_id=project_id)
user_id = get_user_from_memberships(memberships)
if user_id is not None:
return user_id
for project in self._all_projects:
user_id = get_user_from_memberships(project.memberships)
if user_id is not None:
return user_id
return None
def _get_version_id(self, version_name: str) -> Optional[int]:
"""
Method returns ID of version for given project.
:param version_name: name of project version.
:return: version ID.
"""
if self._projects:
for project_id, _ in self._projects:
versions = self._redmine.version.filter(project_id=project_id)
for version in versions:
if version.name == version_name:
return version.id
for project in self._all_projects:
versions = self._redmine.version.filter(project_id=project.id)
for version in versions:
if version.name == version_name:
return version.id
return None
def _parse_info_from_issues_page(self, url: str):
"""
Method parses information from page with issues.
:param url: url address of page with filtered issues.
"""
attempt = 0
html = None
while attempt < MAX_ATTEMPTS_NUMBER:
try:
html = requests.get(url, auth=(self._username, self._password), timeout=3, headers=HEADERS).text
except Exception:
attempt += 1
time.sleep(0.5)
else:
attempt = MAX_ATTEMPTS_NUMBER
if html is None:
return
soup = BeautifulSoup(html, "html.parser")
ps_query_totals = soup.find_all("p", {"class": "query-totals"})
for p_query_totals in ps_query_totals:
for span in p_query_totals.find_all("span", {"class": re.compile("total-for-")}):
for total_option in self._totals_options:
real_option_name = ut.TOTALS_OPTIONS[total_option.lower()].replace("_", "-")
if span["class"][0] == f"total-for-{real_option_name}":
value_span = span.find("span", {"class": "value"})
self._totals_options[total_option] = float(value_span.get_text())
@check_auth
def add_filter(self, filter_name: str, operator_name: str, *values):
"""
Method adds new filter.
:param filter_name: name of filter;
:param operator_name: name of operator for filter;
:param values: values for filter.
"""
filter_name = filter_name.lower()
operator_name = operator_name.lower()
if len(values) == 0:
value = None
elif operator_name in ("между", "between"):
value = values[0]
value_2 = values[1]
else:
value = values[0]
real_filter_name, value = ut.find_real_filter_name_and_value(filter_name, value)
if value is not None and real_filter_name in ut.FILTERS_WITH_USERS:
value = self._get_user_id(value)
elif filter_name in ("версия", "target version"):
value = self._get_version_id(value)
elif filter_name in ("проект", "project"):
project_id = self._find_project_id(value)
self._projects.append((project_id, value))
value = project_id
operator = ut.find_operator(operator_name)
for filter_obj in self._filters:
if (value is None and real_filter_name == filter_obj.get("filter") and
operator == filter_obj.get("operator")):
values = filter_obj.get("values")
if value not in values:
filter_obj["values"].append(value)
return
self._filters.append({"filter": real_filter_name,
"operator": operator,
"values": [] if value is None else [value]})
if operator_name in ("между", "between"):
self._filters[-1]["values"].append(value_2)
def auth(self):
"""
Method authenticates user in Redmine.
"""
self.user = self._redmine.auth()
self._all_projects = self._redmine.project.all()
def clear_filters(self):
"""
Method clears filters.
"""
self._filters = []
self._projects = []
def get_filters(self) -> list:
"""
Method returns list with filters.
:return: list with filters.
"""
return self._filters
@check_auth
def get_groups(self):
"""
Method returns groups. To work with groups in Redmine user must have special
permission.
:return: groups.
"""
groups = self._redmine.group.all()
try:
for _ in groups:
pass
except ForbiddenError:
print("You do not have permission to work with groups")
raise
return groups
@check_auth
def get_project(self, project_name: str) -> Optional[Project]:
"""
Method returns project with given name.
:param project_name: name of project.
:return: project.
"""
for project in self._all_projects:
if project.name == project_name:
try:
project = self._redmine.project.get(project.id)
return project
except Exception:
return None
return None
@check_auth
def get_projects(self) -> List[Tuple[int, str]]:
"""
Method returns IDs and names of all available projects.
:return: IDs and names of available projects.
"""
return [(project.id, project.name) for project in self._redmine.project.all()]
@check_auth
def get_roles(self) -> List[Tuple[int, str]]:
"""
Method returns roles for Redmine users.
:return: list with IDs and names of roles.
"""
return [(role.id, role.name) for role in self._redmine.role.all()]
@check_auth
def get_totals(self, *totals_options) -> Dict[str, Optional[float]]:
"""
Method returns values for given totals options for project issues.
:param totals_options: list with required totals options.
:return: dictionary with values of required options.
"""
self._totals_options = {}
for option in totals_options:
if option.lower() in ut.TOTALS_OPTIONS:
self._totals_options[option] = None
url = ut.create_url(self._filters, self._totals_options)
self._parse_info_from_issues_page(url)
return self._totals_options
@check_auth
def get_users(self):
"""
Method returns users. To work with users in Redmine user must have special
permission.
:return: users.
"""
users = self._redmine.user.all()
try:
for _ in users:
pass
except ForbiddenError:
print("You do not have permission to work with users")
raise
return users
@check_auth
def get_versions_for_project(self, project_name: str) -> List[Tuple[int, str]]:
"""
Method returns versions for project with given name.
:param project_name: name of project for which all versions should be returned.
:return: IDs and names of versions for given project.
"""
project_identifier = self._find_project_id(project_name)
versions = self._redmine.version.filter(project_id=project_identifier)
return [(version.id, version.name) for version in versions]