class JiraInstance: """ Class which is response for creataing FixVersion, marking it as RELEASED and for annote tasks which where done within lastday. """ def __init__(self, config): self.SERVER_ADDRESS = config["SERVER_ADDRESS"] self.USER = config["USER"] self.PASSWORD = config["PASSWORD"] self.PROJECT = config["PROJECT_TAG"] self.CHANGELOG_TAG = config["CHANGELOG_TAG"] self.tasks = [] self.fixedVersion = None options = { 'server': self.SERVER_ADDRESS } self.jira = JIRA(options, basic_auth=(self.USER, self.PASSWORD)) def is_any_new_changeset_in_changelog(self) -> bool: """ Check if there is any new changeset in changelog in Jira task """ changelog_issue = self.jira.issue(self.CHANGELOG_TAG, fields='comment') changelog = self.jira.comments(changelog_issue)[-1] regexp = "{}-([1-9]+[0-9]*)".format(self.PROJECT) for line in changelog.body.splitlines(): issueTag = re.search(regexp, line) if issueTag is None or issueTag is "": continue self.tasks.append(issueTag.group()) return True if len(self.tasks) > 0 else False def add_fixed_version_to_tasks(self, fixed_version): """ Create and add fixed version to tasks from chagnelog task pointed in conf.json """ if fixed_version is None or fixed_version is "": print("Wrong fixed_version") return today = datetime.date.today() today_str = today.strftime('%Y-%m-%d') self.jira.create_version(name=(fixed_version), project=self.PROJECT, released=True, startDate=today_str, releaseDate=today_str) for task in self.tasks: print("Add fixed version to {}".format(task)) issue = self.jira.issue(task, fields='fixVersions') issue.add_field_value('fixVersions', {'name': fixed_version})
def main(): jira = JIRA(**config.JIRA) jql = (f'project = {JIRA_PROJECT} AND type = "Backup & Restore" AND ' f'status NOT IN (Closed, Rejected, Resolved)') opened_issues = [] for issue in jira.search_issues(jql, maxResults=False): comments = jira.comments(issue) opened_issues.append(Issue(issue, comments)) if opened_issues: subject = f'JIRA ({JIRA_PROJECT}) | Active tasks' body = config.SYSINFR_TEMPLATE.render(project=JIRA_PROJECT, issues=opened_issues, wiki=config.SETTINGS['wiki']) email.notify(subject=subject, message=body) logger.info(f'{len(opened_issues)} tasks are found') else: logger.info('nothing is found') jira.close()
def main(): jira = JIRA(**config.JIRA) jql = ( f'project={JIRA_PROJECT} AND summary ~ JobSummary AND status = Open ' f'AND created > startOfDay(-{LOOKUP_DAYS}) AND created < now() ' f'ORDER BY key DESC') opened_issues = defaultdict(list) for issue in jira.search_issues(jql, maxResults=False): services = (config.SETTINGS['sox_services'] + config.SETTINGS['admin_services']) for service_name in services: if service_name in issue.fields.summary: assignee = issue.fields.assignee.name email_domain = config.SETTINGS['smtp']['domain'] email_address = f'{assignee}@{email_domain}' issue.fields.summary = service_name break else: logger.error(f'there is unknown service ({issue.fields.summary})') continue comments = jira.comments(issue) opened_issues[email_address].append(Issue(issue, comments)) for email_address, issues in opened_issues.items(): subject = f'{issues[0].summary} | Backup monitoring' body = config.SOX_TEMPLATE.render(project=JIRA_PROJECT, issues=issues, wiki=config.SETTINGS['wiki']) recipients = config.SMTP_PARAMS['to'] + [email_address] email.notify(subject=subject, to=recipients, message=body) logger.info(f'{len(opened_issues)} tasks are found') if not opened_issues: logger.info('nothing is found') jira.close()
status_to_emoji = { 'Blocked': ':scream:', 'In Progress': ':sunglasses:', 'Open': ':doge:', 'Soft Launch': ':pray:', 'Rollout': ':chart_with_upwards_trend:', 'Done': ':tada:' } lines = [] issue_changes = [] for i in issues: print u'• %s[%s]\t%s' % (i.key.ljust(10), i.fields.status, i.fields.summary) lastcomment = jira.comments(i) if lastcomment: lastcomment = max(lastcomment, key=lambda c: c.updated) print '> ', lastcomment.body print '-' * 40 # Ask if wanting to transition the ticket transition_selection = '' transition_response = raw_input("Would you like to transition this ticket?(y/n) ") if transition_response == 'y': transitions = jira.transitions(i) transition_dict = {} for t in transitions: transition_dict[t['id']] = t['name'] print t['id'], '->', t['name'] transition_options = set(transition_dict.keys())
def main(): # Parse arguments. Assume any error handling happens in parse_args() try: args = parse_args() except Exception as err: print(f"Failed to parse arguments: {err}", file=sys.stderr) start_datetime = args["start_datetime"] end_datetime = args["end_datetime"] detailed = args["detailed"] num_issues_done = 0 num_done_issues_code_reviewed = 0 # Connect to Jira options = {"server": "https://opensciencegrid.atlassian.net"} jira = JIRA(options) # We need to find the key of the "Development" field, to determine if an issue has commits development_field_key = None fields = jira.fields() name_map = {field['name']: field['id'] for field in fields} if "Development" in name_map: development_field_key = name_map["Development"] # Iterate over all completed issues in the HTCONDOR project issues = jira.search_issues( "project = HTCONDOR AND type in (Improvement, Bug) AND status = Done", expand="changelog", maxResults=False) for issue in issues: issue_changelog = issue.changelog.histories issue_isdone = False for change in issue_changelog: for item in change.items: # Issues can be marked Done multiple times, so break after first occurrence if issue_isdone: break # Check if this change was setting the status to Done if item.field == "status" and item.toString == "Done": # Check if this issue has any code commits. If not, don't count it. development_data = getattr(issue.fields, development_field_key) if development_data == "{}": continue # Strip the time offset from the change.created string changed = change.created[0:change.created.rfind("-")] # Was this issue set to Done status between the provided end and start dates? changed_datetime = datetime.strptime( changed, "%Y-%m-%dT%H:%M:%S.%f") if changed_datetime > start_datetime and changed_datetime < end_datetime: if detailed is True: print( f"{issue.key}: {issue.fields.summary}, Marked Done: {changed_datetime.strftime('%Y-%m-%d %H:%M:%S')}" ) num_issues_done += 1 issue_isdone = True # Now check the issue comments for a "code review" text entry comments = jira.comments(issue) if len(comments) > 0: for comment in comments: if "code review" in comment.body.lower()[0:20]: num_done_issues_code_reviewed += 1 if detailed is True: print("\tThis issue was code reviewed") break print( f"\nBetween {start_datetime.strftime('%Y-%m-%d')} and {end_datetime.strftime('%Y-%m-%d')}:\n" ) print(f"{num_issues_done} HTCONDOR issues were marked Done") print( f"{num_done_issues_code_reviewed} of these completed issues were code reviewed" ) if num_issues_done > 0: print( f"Code review rate: {round(num_done_issues_code_reviewed*100/num_issues_done, 2)}%\n" ) else: print(f"No issues marked Done between the dates specified.")
issues = jira.search_issues('project = 10000 AND assignee!=currentUser()', maxResults=0) issue = jira.issue('MYP-6') summary = issue.fields.summary issue.fields.worklog.worklogs issue.fields.worklog.worklogs[0].author issue.fields.worklog.worklogs[0].timeSpent issue.fields.worklog.worklogs[0].updated jira.assign_issue(issue, 'Anakin') transitions = jira.transitions(issue) transition_serie = [(t['id'], t['name']) for t in transitions] comments_b = jira.comments(issue) print(comments_b) comment = jira.comment('MYP-6', '10300') print(comment.body) issue_dict = { 'project': { 'id': 10000 }, 'summary': 'New issue from jira-python', 'description': 'Look into this one', 'issuetype': { 'name': 'Snake' }, } new_issue = jira.create_issue(fields=issue_dict)
class TestIssues(unittest.TestCase): def setUp(self): self.jira = JIRA(options=dict(server=TEST_URL, verify=False), basic_auth=(TEST_USERNAME, TEST_PASSWORD)) self.issue1 = self.jira.create_issue( project='KB', summary='Test-1', issuetype={'name': 'Bug'}, ) self.issue2 = self.jira.create_issue( project='KB', summary='Test-2', issuetype={'name': 'Bug'}, ) def tearDown(self): issues = self.jira.search_issues('project = "KB" AND summary ~ "Test*"', fields=['key']) for _ in issues: _.delete() def assert_single_attachment(self): # TODO - Find how to test this automatically pass def assert_single_comment_with(self, text): comments = self.jira.comments(self.issue1.key) self.assertEqual(len(comments), 1) self.assertIn(text, comments[0].body) def test_new(self): result = CliRunner().invoke(topcli, ['issue', 'new', 'KB', 'task', 'Test-new']) self.assertEqual(result.exit_code, 0) issues = self.jira.search_issues('project = "KB" AND summary ~ "Test-new"', fields=['key', 'summary']) self.assertEqual(len(issues), 1) self.assertIn(issues[0].key, result.output) def test_transition(self): result = CliRunner().invoke(topcli, ['issue', 'transition', self.issue1.key, 'Done']) self.assertEqual(result.exit_code, 0) def test_assign(self): result = CliRunner().invoke(topcli, ['issue', 'assign', self.issue1.key, TEST_USERNAME]) self.assertEqual(result.exit_code, 0) assignee = self.jira.issue(self.issue1.key, fields=['assignee']).fields.assignee self.assertEqual(assignee.key, TEST_USERNAME) def test_unassign(self): result = CliRunner().invoke(topcli, ['issue', 'assign', self.issue1.key, TEST_USERNAME]) result = CliRunner().invoke(topcli, ['issue', 'unassign', self.issue1.key]) self.assertEqual(result.exit_code, 0) assignee = self.jira.issue(self.issue1.key, fields=['assignee']).fields.assignee self.assertIsNone(assignee) def test_attach_file(self): with CliRunner().isolated_filesystem() as dir_path: with open('data.txt', 'w') as f: print('abc', file=f) result = CliRunner().invoke(topcli, ['issue', 'attach', self.issue1.key, 'data.txt']) self.assertEqual(result.exit_code, 0) self.assert_single_attachment() def test_comment_args(self): result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key, 'Comment', 'from args']) self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from args') def test_comment_file(self): with CliRunner().isolated_filesystem() as dir_path: with open('comment.txt', 'w') as f: print('Comment from file', file=f) result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key, 'comment.txt']) self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from file') def test_comment_prompt(self): result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key], input='Comment from prompt\n') self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from prompt') def test_comment_stdin(self): result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key, '-'], input='Comment\nfrom\nstdin') self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment\nfrom\nstdin') def test_link(self): result = CliRunner().invoke(topcli, ['issue', 'link', self.issue1.key, self.issue2.key, '-t', 'duplicates']) self.assertEqual(result.exit_code, 0) links = self.jira.issue(self.issue1.key, fields=['issuelinks']).fields.issuelinks self.assertEqual(len(links), 1) self.assertEqual(links[0].outwardIssue.key, self.issue2.key) self.assertEqual(links[0].type.outward, 'duplicates') def test_unlink(self): result = CliRunner().invoke(topcli, ['issue', 'link', self.issue1.key, self.issue2.key, '-t', 'duplicates']) self.assertEqual(result.exit_code, 0) result = CliRunner().invoke(topcli, ['issue', 'unlink', self.issue1.key, self.issue2.key]) links = self.jira.issue(self.issue1.key, fields=['issuelinks']).fields.issuelinks self.assertEqual(len(links), 0) def test_search_issue(self): result = CliRunner().invoke(topcli, ['issue', 'search']) self.assertEqual(result.exit_code, 0) self.assertIn('KB-1', result.output) self.assertIn('KB-2', result.output) self.assertIn('KB-3', result.output)
class JiraCI: resolution_state = {"fixed": "1", "wont fixed": "2", "duplicate": "3", "incomplete": "4", "cannot reproduce": "5", "not a bug": "6", "done": "7"} def __init__(self, jira_url, login, password): if version_info[1] <= 6: options = jira_url else: options = {"server": jira_url} self.jira = JIRA(options, basic_auth=(login, password)) @staticmethod def debug_jira(text): stdout.write("[DEBUG JIRA]: {0}\n".format(text)) def check_issue_exist(self, issue_id): try: self.jira.issue(issue_id) except JIRAError as e: print "[-] : {0} - {1}".format(issue_id, e.text) return False else: return True def check_issue_state(self, issue_id, issue_state): jira_issue = self.jira.issue(issue_id) if jira_issue.fields.status.name.lower() == issue_state.lower(): return True else: return False def add_comment(self, issue_id, comment, formatting=False): jira_issue = self.jira.issue(issue_id) if formatting: comment = "{code}" + comment + "{code}" if not self.check_comment_exist(issue_id, comment): self.jira.add_comment(jira_issue, comment) self.debug_jira("Comment (for {0}) : {1} added".format(issue_id, comment.rstrip())) else: self.debug_jira("Comment (for {0}) : {1} already exist".format(issue_id, comment.rstrip())) def assign_issue(self, issue_id, assigned_user): jira_issue = self.jira.issue(issue_id) jira_issue.update(assignee={"name": assigned_user}) def add_link(self, issue_id, title, url): url_object = {"url": url, "title": title} if not self.check_link_exist(issue_id, title, url): self.jira.add_remote_link(issue_id, url_object) self.debug_jira("Link (for {0}) : {1} added".format(issue_id, url)) else: self.debug_jira("Link (for {0}) : {1} already exist".format(issue_id, url)) def resolve_issue_to_reporter(self, issue_id): reporter = self.get_reporter_issue(issue_id) self.jira.transition_issue(issue_id, "5", resolution={"id": self.resolution_state["fixed"]}) self.assign_issue(issue_id, reporter) def get_reporter_issue(self, issue_id): jira_issue = self.jira.issue(issue_id) return jira_issue.fields.reporter.name def check_comment_exist(self, issue_id, new_comment): comments = [c.body for c in self.jira.comments(issue_id)] if new_comment in comments: return True return False def check_link_exist(self, issue_id, title, url): links = [l.raw["object"] for l in self.jira.remote_links(issue_id)] for link in links: if link["title"] == title and link["url"] == url: return True return False def resolve_from_git(self, issue_id, short_commit_message, title_url, package_url): if self.check_issue_exist(issue_id): if not self.check_issue_state(issue_id, "resolved"): self.resolve_issue_to_reporter(issue_id) self.debug_jira("Issue {0} already resolve".format(issue_id)) else: self.debug_jira("Issue {0} resolved".format(issue_id)) self.add_link(issue_id, title_url, package_url) self.add_comment(issue_id, short_commit_message, formatting=True) def refer_from_git(self, issue_id, commit_message): if self.check_issue_exist(issue_id): self.add_comment(issue_id, commit_message, formatting=True)
class JiraTool(): def __init__(self, server, username, password, maxResults = 500): self.server = server self.basic_auth = (username, password) # issues查询的最大值 self.maxResults = maxResults def login(self): self.jira = JIRA(server=self.server, basic_auth=self.basic_auth) if self.jira == None: print('连接失败') sys.exit(-1) def get_projects(self): """ 获得jira 的所有项目 :return: """ return [(p.key, p.name, p.id) for p in self.jira.projects()] def get_components(self, project): """ 获得某项目的所有模块 :param project: :return: """ return [(c.name, c.id) for c in self.jira.project_components(self.jira.project(project))] def create_component(self, project, compoment, description, leadUserName=None, assigneeType=None, isAssigneeTypeValid=False): """ # 创建项目模块 :param project: 模块所属项目 :param compoment:模块名称 :param description:模块描述 :param leadUserName: :param assigneeType: :param isAssigneeTypeValid: :return: """ components = self.jira.project_components(self.jira.project(project)) if compoment not in [c.name for c in components]: self.jira.create_component(compoment, project, description=description, leadUserName=leadUserName, assigneeType=assigneeType, isAssigneeTypeValid=isAssigneeTypeValid) def create_issue(self, project, compoment, summary, description, assignee, issuetype, priority='Medium'): """ 创建提交bug :param project: 项目 :param issuetype: 问题类型,Task :param summary: 主题 :param compoment: 模块 :param description: 描述 :param assignee: 经办人 :param priority: 优先级 :return: """ issue_dict = { 'project': {'key': project}, 'issuetype': {'id': issuetype}, 'summary': summary, 'components': [{'name': compoment}], 'description': description, 'assignee': {'name': assignee}, 'priority': {'name': priority}, } return self.jira.create_issue(issue_dict) def delete_issue(self, issue): """ 删除bug :param issue: :return: """ issue.delete() def update_issue_content(self, issue, issue_dict): """ 更新bug内容 :param issue: :param issue_dict: issue_dict = { 'project': {'key': project}, 'issuetype': {'id': issuetype}, 'summary': summary, 'components': [{'name': compoment}], 'description': description, 'assignee': {'name': assignee}, 'priority': {'name': priority}, } :return: """ issue.update(fields=issue_dict) def update_issue_issuetype(self, issue, issuetype): """ 更新bug 状态 :param issue: :param issuetype: 可以为id值如11,可以为值如'恢复开启问题' :return: """ transitions = self.jira.transitions(issue) # print([(t['id'], t['name']) for t in transitions]) self.jira.transition_issue(issue, issuetype) def search_issues(self, jql): """ 查询bug :param jql: 查询语句, 如"project=项目key AND component = 模块 AND status=closed AND summary ~标题 AND description ~描述" :return: """ try: # maxResults参数是设置返回数据的最大值,默认是50。 issues = self.jira.search_issues(jql, maxResults=self.maxResults) except Exception as e: print(e) sys.exit(-1) return issues def search_issue_content(self, issue, content_type): """ 获取bug 的相关信息 :param issue: :param content_type:项目project; 模块名称components; 标题summary; 缺陷类型issuetype; 具体描述内容description; 经办人assignee; 报告人reporter; 解决结果resolution; bug状态status; 优先级priority; 创建时间created; 更新时间updated; 评论comments :return: """ # 评论 if content_type == 'comments': return [c.body for c in self.jira.comments(issue)] if hasattr(issue.fields, content_type): result = getattr(issue.fields, content_type) if isinstance(result, list): return [c.name for c in result if hasattr(c, 'name')] return result def collect_bug_report(self, project, root_path): """ 搜集bug报告上模块信息 :param project: :param root_path: :return: """ import os statistics_path = root_path + '/BugReport/statistics/' reports_path = root_path + '/BugReport/reports/' + project + '/' if not os.path.exists(statistics_path): os.makedirs(statistics_path) if not os.path.exists(reports_path): os.makedirs(reports_path) text = 'id, Key, Type, Status, Resolution, Priority, components, AffectsVersions, FixedVersions, Reporter, ' \ 'Creator, Assignee, CreatedDate, ResolutionDate, UpdatedDate, Summary\n' search_str = 'project = ' + project.upper() + ' AND issuetype = Bug AND status in (Resolved, Closed) AND ' \ 'resolution in (Fixed, Resolved) ORDER BY key ASC' start = 0 max_results_each_search = 1000 while True: try: # maxResults参数是设置返回数据的最大值,默认是50。 issues = self.jira.search_issues(search_str, startAt=start, maxResults=max_results_each_search) except Exception as e: print(e) sys.exit(-1) for issue in issues: text += issue.id + ',' text += issue.key + ',' text += issue.fields.issuetype.name + ',' text += issue.fields.status.name + ',' text += issue.fields.resolution.name + ',' priority_name = 'Unassigned' if issue.fields.priority is None else issue.fields.priority.name text += priority_name + ',' components = issue.fields.components if len(components) > 0: for component in components: text += component.name + '|' text += ',' if hasattr(issue.fields, 'versions'): for version in issue.fields.versions: text += version.name + '|' text += ',' for version in issue.fields.fixVersions: text += version.name + '|' text += ',' reporterName = 'Unassigned' if issue.fields.reporter is None else issue.fields.reporter.displayName creatorName = 'Unassigned' if issue.fields.creator is None else issue.fields.creator.displayName assigneeName = 'Unassigned' if issue.fields.assignee is None else issue.fields.assignee.displayName text += reporterName + ',' text += creatorName + ',' text += assigneeName + ',' text += issue.fields.created + ',' text += issue.fields.resolutiondate + ',' text += issue.fields.updated + ',' text += issue.fields.summary + ',' # 输出报告内容 with open(reports_path + issue.key + '.txt', 'w', encoding='utf-8') as fw: description = 'Summary:\n' + issue.fields.summary + '\nDescription:\n' description += '' if issue.fields.description is None else issue.fields.description fw.write(description) fw.close() text += '\n' start += max_results_each_search print(start) if start > issues.total: break # 输出项目统计信息 with open(statistics_path + project + '.csv', 'w', encoding='utf-8') as fw: fw.write(text) fw.close() print('The collection for bug reports of Project ' + project + ' has finished!')
class JiradoistSyncher(object): def __init__(self): while True: try: self._setup_jira() self._setup_todoist() break except Exception as e: print "Setup failed, trying again in 10 s" print e time.sleep(10) def _setup_jira(self): with open(os.path.join(PASSWORD_BASE, 'jira_'), 'r') as config: access_token, access_token_secret = config.read().strip().split( ',') with open(os.path.join(PASSWORD_BASE, JIRA_KEYFILE), 'r') as keyf: key_cert = keyf.read() oauth_dict = { 'access_token': access_token, 'access_token_secret': access_token_secret, 'consumer_key': 'jira-api-tests', 'key_cert': key_cert } self.jira = JIRA(oauth=oauth_dict) def _setup_todoist(self): self.clear_temp() # Set up Todoist with open(os.path.join(PASSWORD_BASE, 'todoist_'), 'r') as config: self.td_api = TodoistAPI(config.read().strip()) self.safe_sync() def get_or_create_label(self, text, color): label = next((label for label in self.td_api.labels.all() if label['name'] == text and label['color'] == color), None) if not label: label = self.td_api.labels.add(text, color=color) return label def update_labels(self, item, issue): status = issue.fields.status.name label = self.get_or_create_label(status, label_colors.get(status, colors.GRAY))['id'] item.update(labels=[label]) def update_urgency(self, item, issue): item.update(priority=priority_mapping[issue.fields.priority.id]) def update_comments(self, item, issue): notes = [ note['content'] for note in self.td_api.notes.all() if note['item_id'] == item['id'] ] for comment in self.jira.comments(issue): text = self.text_from_jira_comment(comment) if text not in notes: note = self.td_api.notes.add(item['id'], text) print u"Added note to ticket {}: {}".format(issue.key, text) def text_from_jira_comment(self, comment): return comment.raw['author']['displayName'] + ": " + comment.body def clear_temp(self): if os.path.exists(os.path.expanduser('~/.todoist-sync')): shutil.rmtree(os.path.expanduser('~/.todoist-sync')) def sync(self): for jql, target_project in JQL_PROJECT_MAPPING.iteritems(): print "Synching {} <-- {}".format(target_project, jql) try: proj_id = next(proj.data['id'] for proj in self.td_api.projects.all() if proj.data['name'] == target_project) except StopIteration: print "Cannot find target project {} in todoist".format( target_project) continue try: issues = search_all_issues(self.jira, jql) except JIRAError: print "Cannot evaluate JQL expression {}".format(jql) continue items = [ item for item in self.td_api.items.all() if item.data['project_id'] == proj_id ] keys = [item.data['content'].split(' ')[0] for item in items] jira_keys = [issue.key for issue in issues] for issue in issues: if issue.key not in keys: item = self.td_api.items.add(u'{} {} {}/browse/{}'.format( issue.key, issue.fields.summary, JIRA_SERVER, issue.key), project_id=proj_id) print u"Adding task {} {}".format(issue.key, issue.fields.summary) items.append(item) keys.append(issue.key) item = items[keys.index(issue.key)] self.update_labels(item, issue) self.update_urgency(item, issue) self.update_comments(item, issue) for item in items: if item.data['content'].split(' ')[0] not in jira_keys: item.delete() print u"Deleting task {}".format(item.data['content']) self.safe_sync() def safe_sync(self): try: self.td_api.commit() print "Committed.." self.td_api.sync() print "...aaand synched!" except (todoist.api.SyncError, requests.exceptions.ConnectionError) as e: print "Couldn't sync, restarting connection to todoist..." print e self._setup_todoist()
def create_bug_issue(self, channel, summary, description, component, version, labels, attachments={}, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT, DRY_RUN=False): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ def add_attachments(jira, ticketId, attachments): for file in attachments: urlretrieve(attachments[file], file) jira.add_attachment(ticketId, os.getcwd() + '/' + file, file) os.unlink(file) if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd), options=dict(verify=False)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error( 'Did not provide either a Jira user, a Jira password or a Jira project' ) return # Check for existing bugs for the same test case, if there are any, suppress filing another test_case = summary.split(' ')[0] existing = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed' % test_case) if len(existing) > 0: self.logger.info('Found open issue(s) for "' + test_case + '" ' + ' '.join([k.key for k in existing])) # Check if new failure is on different job than existing ticket, if so comments job = summary.split()[-2] existing_ticket = jira.issue(existing[0].id) if job not in existing_ticket.fields.summary: comments = jira.comments(existing[0].id) for comment in comments: # Check for existing comment for same job, if there are any, suppress commenting another if job in comment.body: self.logger.info('Found existing comment(s) for "' + job + '" on open issue') return self.logger.info( 'Commenting about separate job failure for %s on open issue' % test_case) if not DRY_RUN: jira.add_comment(existing[0].id, summary + '\n\n' + description) add_attachments(jira, existing[0].id, attachments) return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) version = 'V' + version for v in versions: if str(v.name) == version.strip(): jira_version = {'name': v.name, 'id': v.id} break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] issue_dict['fixVersions'] = [{'name': 'Backlog'}] issue_dict['priority'] = {'name': 'Blocker'} self.logger.info("Filing ticket: %s" % summary) if not DRY_RUN: new_issue = jira.create_issue(fields=issue_dict) add_attachments(jira, new_issue.id, attachments) #self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message( channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key) suite = summary.split('.')[-3] # Find all tickets within same test suite and link them link_tickets = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed and reporter in (voltdbci)' % suite) for ticket in link_tickets: jira.create_issue_link('Related', new_issue.key, ticket) else: new_issue = None return new_issue
class jira_project_dataframes: def __init__(self, server, user, auth, project_key, data_directory_path): self.project_key = project_key # settings dataframe should probably be abstracted to a setting file # since issue data parts rarely change, we're building for "in-code" visibility self.data_part_settings = pd.DataFrame({ 'data_part': [ 'issues', 'issue_components', 'issue_labels', 'issue_stakeholders', 'issue_worklogs', 'issue_comments' ], 'key_name': [ 'issue_key', 'issue_component_key', 'issue_label_key', 'issue_stakeholder_key', 'issue_worklog_id', 'issue_comments_id' ], 'file_name': [ 'ISSUES.csv', 'ISSUES_COMPONENTS.csv', 'ISSUES_LABELS.csv', 'ISSUES_STAKEHOLDERS.csv', 'ISSUES_WORKLOGS.csv', 'ISSUES_COMMENTS.csv' ] }) # placeholder for storing dataframes # NOTE: DATAFRAMES CAN NOT REALLY BE STORED WITHIN OTHER DATAFRAMES # THIS IS WHY DATAFRAMES ARE NOT COMBINED WITH data_part_settings # TO ACCESS DATAFRAMES, USE THE data_part_name like this: # comments = jdf.data_part_dataframes['issue_comments'] self.data_part_dataframes = {} self.jira_server = server self.jira_user = user self.jira_auth = auth self.jira = JIRA(options={'server': self.jira_server}, basic_auth=(self.jira_user, self.jira_auth)) self.issues = [] self.data_directory_path = os.path.abspath(data_directory_path) self.make_project_data_directory() def string_to_datetime(self, x): """ Returns a datetime for a given string. keyword arguments: x: date string """ return_value = "" if x: return_value = dtup.parse(x) return return_value def make_delimit_ready(self, x): """ Jira can contain a bunch of characters that will affect the delimiting of dataframes that are saved to .csv files such as carraige returns in the issue comments, description, or worklog """ return_value = "" if x: x = str(x) x = x.replace( u"\u201c", '"') #Windows left smart double quote with simple double quote x = x.replace( u"\u201d", '"' ) #Windows right smart double quote with simple double quote x = x.replace( u"\u2018", "'") #Windows left smart single quote with simple single quote x = x.replace( u"\u2019", "'" ) #Windows right smart single quote with simple single quote x = x.replace(u"\u000C", "_") #Tab with underscore x = x.replace(u"\u000D", "-") #Carraige Return with dash x = x.replace(u"\u000A", "-") #Line Feed with dash return_value = str(x).replace(u"\u201c", '"').replace( u"\u201d", '"').replace(u"\u2018", "'").replace( u"\u2019", "'").replace(r"\r\n", "-").replace(r"\r", "---") return return_value def ifnull(self, value, null_value=None): """ Returns value or null_value if an error is raise or it is "false" Note: empty strings are not NULL keyword arguments: value: a value to interrogate for null null_value: a value to substitute if the given value is null """ return_value = null_value try: if type(value) is None: return_value = value else: return_value = null_value except: return_value = null_value return return_value def make_project_data_directory(self): """ Creates and/or Returns the data directory for the project that is set in the class self.project_key """ try: os.mkdir(self.data_directory_path) except: "Do Nothing" return True def get_data_part_element(self, data_part_name, element_name): """ Returns the value of the element of a data part keyword arguments: data_part_name: the name of a data_part element_name: the name of the element to return """ data_part = self.data_part_settings.loc[ self.data_part_settings['data_part'] == data_part_name, element_name] return data_part.iloc[0] def file_to_dataframe(self, file_path): """ Retrieves a file as a dataframe. If no file is found with the given file_path, then returns None keyword arguments: file_path: the full path to the csv file to open """ return_df = None try: return_df = pd.read_csv(file_path) except FileNotFoundError: ist.msg( "file_to_dataframe(): data file was not found: '" + file_path + "'", True) return_df = None return return_df def get_file_path_for_data_part(self, data_part_name): """ Retrieves the file name from the data_file_name dictionary for a given data part name keyword arguments: data_part_name: the name of an issue data part defined in the data_part_settings dataframe """ return_value = "" try: file_name = self.project_key + "_" + self.get_data_part_element( data_part_name, 'file_name') file_path = os.path.abspath(self.data_directory_path + "/" + file_name) return_value = file_path except: return_value = "" #ist.msg("get_file_path_for_data_part(): file_path is '" + return_value + "'",True) return return_value def get_issues_file_mod_date(self): """ Returns the modified date from the ISSUES.csv file using If file is not found, returns an empty string """ file_path = self.get_file_path_for_data_part('issues') date_string = '' if os.path.isfile(file_path): timestamp_modified_on = os.path.getmtime(file_path) date_modified_on = dt.fromtimestamp(timestamp_modified_on) date_modified_on = date_modified_on + timedelta(days=-1) date_string = dt.strftime(date_modified_on, "%Y-%m-%d") return date_string def retrieve_issues(self, jql, max_results=0): """ Retrives all issues for a jira query language query string up to a specified number of results keyword arguments: jql: a jira query language script to pass to Jira max_results (default 0): the maximum results to return; 0 is unlimited """ issues = self.jira.search_issues(jql_str=jql, maxResults=max_results) self.issues = issues ist.msg(str(len(issues)) + " issues retrieved", True) return issues def retrieve_latest_issues(self, min_updated_date): """ Retrieves all issues that have been updated since the given minimum updated date keyword arguments: min_updated_date: the beginning date to consider issues to retrieve """ min_updated_date = ist.convert_date_string(min_updated_date, '%F') jql = "(project = " + self.project_key + ") and (updatedDate >= " + min_updated_date + " or worklogDate >= " + min_updated_date + ")" issues = self.retrieve_issues(jql) return issues def data_part_to_dataframe(self, issues, data_part_name): """ Converts a set of JIRA.issues to a dataframe based on the data_part_name provided. **NOTE, THESE ARE A HARD CODED SET OF COLUMNS** **THERE MAY BE MORE COLUMNS AVAILABLE** keyword arguments: issues: a list of JIRA.issue objects data_part_name: the data part to return as a dataframe """ return_dataframe = pd.DataFrame() if data_part_name == 'issues': for issue in issues: i = len(return_dataframe) + 1 return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[i, 'summary'] = issue.fields.summary return_dataframe.loc[i, 'description'] = self.make_delimit_ready( issue.fields.description) return_dataframe.loc[i, 'priority'] = str(issue.fields.priority) return_dataframe.loc[i, 'issue_type'] = str( issue.fields.issuetype) return_dataframe.loc[i, 'status'] = str(issue.fields.status.name) return_dataframe.loc[i, 'stakeholders'] = ",".join( self.ifnull(issue.fields.customfield_10800, [""])) return_dataframe.loc[i, 'create_date'] = self.string_to_datetime( issue.fields.created) return_dataframe.loc[i, 'due_date'] = self.string_to_datetime( issue.fields.duedate) return_dataframe.loc[i, 'last_viewed'] = self.string_to_datetime( issue.fields.lastViewed) return_dataframe.loc[ i, 'resolution_date'] = self.string_to_datetime( issue.fields.resolutiondate) return_dataframe.loc[i, 'resolution'] = str( issue.fields.resolution) return_dataframe.loc[ i, 'total_seconds_spent'] = issue.fields.aggregatetimespent return_dataframe.loc[i, 'assignee'] = str(issue.fields.assignee) return_dataframe.loc[i, 'reporter'] = str(issue.fields.reporter) return_dataframe.loc[i, 'components'] = ",".join( [str(component) for component in issue.fields.components]) return_dataframe.loc[i, 'labels'] = ",".join( [str(label) for label in issue.fields.labels]) elif data_part_name == 'issue_components': for issue in issues: for component in [ str(component) for component in issue.fields.components ]: i = len(return_dataframe) + 1 return_dataframe.loc[i, 'issue_component_key'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[ i, 'component'] = self.make_delimit_ready(component) elif data_part_name == 'issue_stakeholders': for issue in issues: if issue.fields.customfield_10800: for stakeholder in [ str(stakeholder) for stakeholder in issue.fields.customfield_10800 ]: i = len(return_dataframe) + 1 return_dataframe.loc[i, 'issue_stakeholder_key'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[ i, 'stakeholder'] = self.make_delimit_ready( stakeholder) elif data_part_name == 'issue_labels': for issue in issues: for label in [str(label) for label in issue.fields.labels]: i = len(return_dataframe) + 1 return_dataframe.loc[i, 'issue_label_key'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[i, 'label'] = self.make_delimit_ready( label) elif data_part_name == 'issue_worklogs': for issue in issues: for worklog in self.jira.worklogs(issue): i = len(return_dataframe) + 1 return_dataframe.loc[i, 'issue_worklog_id'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[i, 'author'] = worklog.author.displayName return_dataframe.loc[i, 'created'] = self.string_to_datetime( worklog.created) return_dataframe.loc[i, 'started'] = self.string_to_datetime( worklog.started) return_dataframe.loc[ i, 'seconds_spent'] = worklog.timeSpentSeconds elif data_part_name == 'issue_comments': for issue in issues: for comment in self.jira.comments(issue): i = len(return_dataframe) + 1 ## it turns out that a comment sent to the Jira from an email ## that has an error will result in no author being included ## and will show as "Anonymous" in the Jira web application ## this causes a missing author attribute in the result of ## jira.comments(issue) try: return_dataframe.loc[i, 'issue_comments_id'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[ i, 'author'] = comment.author.displayName return_dataframe.loc[ i, 'created'] = self.string_to_datetime( comment.created) return_dataframe.loc[ i, 'body_text'] = self.make_delimit_ready( comment.body) except AttributeError: return_dataframe.loc[i, 'issue_comments_id'] = i return_dataframe.loc[i, 'issue_key'] = issue.key return_dataframe.loc[i, 'author'] = 'Unknown' return_dataframe.loc[ i, 'created'] = self.string_to_datetime( comment.created) return_dataframe.loc[ i, 'body_text'] = self.make_delimit_ready( comment.body) else: raise ValueError("The data_part_name given: '" + data_part_name + "' does not have a dataframe definition.") ist.msg( str(len(return_dataframe)) + " records converted for " + data_part_name, True) return return_dataframe def update_dataframe(self, existing_df, updated_df, key_column): """ Returns an given dataframe updated with values of a second given dataframe based on a given key. If no updated_df is given or there are 0 records in updated_df, then the existing_df is returned. keyword arguments: existing_df: a dataframe that contains the existing information updated_df: a dataframe that contains the updated information key_column: the string name of the column that is the key for both """ ### technique used is to concatenate (union) rows of both dataframes, ### then deduplicate based on the given key return_df = None existing = ist.dataframe_has_rows(existing_df) updated = ist.dataframe_has_rows(updated_df) # if we have an existing and updated then update the existing with updated if updated & existing: # get all issue keys from the updated_df issue_keys = list(set(updated_df['issue_key'].tolist())) # Remove any records where the issue was modified. existing_df = existing_df[~existing_df['issue_key'].isin(issue_keys )] #ist.msg("updated_dataframe(): updating existing dataframe with updated records",True) return_df = pd.concat([updated_df, existing_df], sort=False) return_df = return_df.drop_duplicates(key_column) return_df = return_df.sort_values(key_column) return_df = return_df.set_index(key_column) return_df = return_df.reset_index() # if we only have an updated_df, then return the updated_df elif (updated) & (not existing): ist.msg( "updated_dataframe(): missing existing dataframe; returning updated dataframe", True) return_df = updated_df # if we only have an existing_df then return the existing_df elif (not updated) & (existing): ist.msg( "updated_dataframe(): missing updated dataframe; returning existing dataframe", True) return_df = existing_df # we don't have an updated or existing - something's wrong else: ist.msg( "update_dataframe(): there is neither an existing nor updated dataframe passed", True) # if an updated_df is None (e.g. there was no file found) return return_df def refresh_data_part(self, data_part_name, issues): """ Refreshes a data part's information based on the updated_date given keyword arguments: data_part_given: the data part to refresh issues: the issues to refresh the data part with """ # get info about the data part file_path = self.get_file_path_for_data_part(data_part_name) key_name = self.get_data_part_element(data_part_name, 'key_name') #convert to dataframes and save updated_df = self.data_part_to_dataframe(issues, data_part_name) existing_df = self.file_to_dataframe(file_path) final_df = self.update_dataframe(existing_df, updated_df, key_name) # save final dataframe self.data_part_dataframes[data_part_name] = final_df final_df.to_csv(file_path, index=False) return final_df def get_project_data(self, from_date=None): """ Refreshes the project data for any issues that have a modified date on or after the given from_date. If no updated_date is given, the from_date will be one day before the modification date of the save ISSUES.csv file. If no file exists, all issues for the project will be retrieved and saved. keyword arguments: from_date (default None): the minimum issue modified date to retrieve updated project data for """ if not self.project_key: raise ValueError( "A project_key is required to refresh the project information. Update the project_key attribute and rerun." ) start = dt.now() ist.msg("###################################################") ist.msg("Begin updating issues...", True) #retrieve issues from Jira based on the updatedDate given #if no updated_date is given, get update date from ISSUES.csv modification date if from_date is None: ist.msg("No update_date given, using file modified date instead", True) from_date = self.get_issues_file_mod_date() #if no file exists (i.e. updated_date = ""), then get all issues for the project if not from_date: #pull full set of issues for the project ist.msg( "No issues file found. Retrieving all issues for the project.", True) issues = self.retrieve_issues(jql="project=" + self.project_key, max_results=0) else: #pull just the latest issues ist.msg("Retrieving issues modified on or after: " + from_date) issues = self.retrieve_latest_issues(from_date) # iterate through all data_parts and update issues for data_part in self.data_part_settings['data_part'].to_list(): ist.msg("Updating data_part " + data_part, True) self.refresh_data_part(data_part, issues) ist.msg("Issue Information update completed...", True, start) ist.msg("###################################################") return True def issue_to_dataframe_all_fields(self, issue): """ Returns a dataframe with all available fields in a Jira Issue object !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! this method is a way to inspect all of the fields in a jira issues object !! for quick reference. !! !! Please note, the value may need to be parsed as a nested Jira object !! For full reference, see the documentation at: !! https://jira.readthedocs.io/en/master/ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! PLEASE NOTE: INSPECTION OF THE RETURNED DATAFRAME FROM A VARIABLE EXPLORER MAY RESULT IN A RECURSSION ERRROR DUE TO SOME FIELDS CONTAINING MULTIPLE JIRA OBJECTS. """ df = pd.DataFrame({'issue_key': [0]}) df['issue_key'] = issue.key df['aggregateprogress'] = self.ifnull(issue.fields.aggregateprogress) df['aggregatetimeestimate'] = self.ifnull( issue.fields.aggregatetimeestimate) df['aggregatetimeoriginalestimate'] = self.ifnull( issue.fields.aggregatetimeoriginalestimate) df['aggregatetimespent'] = self.ifnull(issue.fields.aggregatetimespent) df['assignee'] = self.ifnull(issue.fields.assignee) df['components'] = self.ifnull(issue.fields.components) df['created'] = self.ifnull(issue.fields.created) df['creator'] = self.ifnull(issue.fields.creator) df['customfield_10000'] = self.ifnull(issue.fields.customfield_10000) df['customfield_10001'] = self.ifnull(issue.fields.customfield_10001) df['customfield_10002'] = self.ifnull(issue.fields.customfield_10002) df['customfield_10003'] = self.ifnull(issue.fields.customfield_10003) df['customfield_10006'] = self.ifnull(issue.fields.customfield_10006) df['customfield_10007'] = self.ifnull(issue.fields.customfield_10007) df['customfield_10011'] = self.ifnull(issue.fields.customfield_10011) df['customfield_10025'] = self.ifnull(issue.fields.customfield_10025) df['customfield_10100'] = self.ifnull(issue.fields.customfield_10100) df['customfield_10101'] = self.ifnull(issue.fields.customfield_10101) df['customfield_10102'] = self.ifnull(issue.fields.customfield_10102) df['customfield_10104'] = self.ifnull(issue.fields.customfield_10104) df['customfield_10105'] = self.ifnull(issue.fields.customfield_10105) df['customfield_10300'] = self.ifnull(issue.fields.customfield_10300) df['customfield_10400'] = self.ifnull(issue.fields.customfield_10400) df['customfield_10500'] = self.ifnull(issue.fields.customfield_10500) df['customfield_10600'] = self.ifnull(issue.fields.customfield_10600) df['customfield_10700'] = self.ifnull(issue.fields.customfield_10700) df['customfield_10800'] = self.ifnull(issue.fields.customfield_10800) df['customfield_10801'] = self.ifnull(issue.fields.customfield_10801) df['customfield_10804'] = self.ifnull(issue.fields.customfield_10804) df['customfield_10805'] = self.ifnull(issue.fields.customfield_10805) df['customfield_10807'] = self.ifnull(issue.fields.customfield_10807) df['customfield_10808'] = self.ifnull(issue.fields.customfield_10808) df['customfield_10809'] = self.ifnull(issue.fields.customfield_10809) df['customfield_10812'] = self.ifnull(issue.fields.customfield_10812) df['customfield_10813'] = self.ifnull(issue.fields.customfield_10813) df['customfield_10814'] = self.ifnull(issue.fields.customfield_10814) df['customfield_10815'] = self.ifnull(issue.fields.customfield_10815) df['customfield_10817'] = self.ifnull(issue.fields.customfield_10817) df['customfield_10818'] = self.ifnull(issue.fields.customfield_10818) df['customfield_10819'] = self.ifnull(issue.fields.customfield_10819) df['customfield_10820'] = self.ifnull(issue.fields.customfield_10820) df['customfield_10821'] = self.ifnull(issue.fields.customfield_10821) df['customfield_10822'] = self.ifnull(issue.fields.customfield_10822) df['customfield_10823'] = self.ifnull(issue.fields.customfield_10823) df['customfield_10824'] = self.ifnull(issue.fields.customfield_10824) df['customfield_10825'] = self.ifnull(issue.fields.customfield_10825) df['customfield_10826'] = self.ifnull(issue.fields.customfield_10826) df['customfield_10827'] = self.ifnull(issue.fields.customfield_10827) df['customfield_10828'] = self.ifnull(issue.fields.customfield_10828) df['description'] = self.ifnull(issue.fields.description) df['duedate'] = self.ifnull(issue.fields.duedate) df['environment'] = self.ifnull(issue.fields.environment) df['fixVersions'] = self.ifnull(issue.fields.fixVersions) df['issuelinks'] = self.ifnull(issue.fields.issuelinks) df['issuetype'] = self.ifnull(issue.fields.issuetype) df['labels'] = self.ifnull(issue.fields.labels) df['lastViewed'] = self.ifnull(issue.fields.lastViewed) df['priority'] = self.ifnull(issue.fields.priority) df['progress'] = self.ifnull(issue.fields.progress) df['project'] = self.ifnull(issue.fields.project) df['reporter'] = self.ifnull(issue.fields.reporter) df['resolution'] = self.ifnull(issue.fields.resolution) df['resolutiondate'] = self.ifnull(issue.fields.resolutiondate) df['security'] = self.ifnull(issue.fields.security) df['status'] = self.ifnull(issue.fields.status) df['statuscategorychangedate'] = self.ifnull( issue.fields.statuscategorychangedate) df['subtasks'] = self.ifnull(issue.fields.subtasks) df['summary'] = self.ifnull(issue.fields.summary) df['timeestimate'] = self.ifnull(issue.fields.timeestimate) df['timeoriginalestimate'] = self.ifnull( issue.fields.timeoriginalestimate) df['timespent'] = self.ifnull(issue.fields.timespent) df['updated'] = self.ifnull(issue.fields.updated) df['versions'] = self.ifnull(issue.fields.versions) df['votes'] = self.ifnull(issue.fields.votes) df['watches'] = self.ifnull(issue.fields.watches) df['workratio'] = self.ifnull(issue.fields.workratio) return df
class Util(object): """ """ def __init__(self, **kwargs): """ """ if 'config' in kwargs: self._config = kwargs['config'] else: logging.critical("config was not defined") raise Exception("config was not defined") if 'username' in kwargs: self._username = kwargs['username'] else: logging.critical("username was not defined") raise Exception("username was not defined") if 'password' in kwargs: self._password = kwargs['password'] else: logging.critical("password was not defined") raise Exception("password was not defined") if 'project' in kwargs: self._project = kwargs['project'] else: if 'project' in self._config: self._project = self._config['project'] logging.info( "project was set to '%s' from the configuration file" % self._project) else: self._project = DEFAULT_PROJECT logging.info("project was set to default '%s'" % self._project) if 'base_url' in kwargs: self._base_url = kwargs['base_url'] else: if 'base_url' in self._config: self._base_url = self._config['base_url'] logging.info( "base_url was set to '%s' from the configuration file" % self._base_url) else: self._base_url = DEFAULT_BASE_URL logging.info("base_url was set to default '%s'" % self._base_url) if 'add_missing_watchers' in kwargs: self._add_missing_watchers = kwargs['add_missing_watchers'] else: if 'add_missing_watchers' in self._config: self._add_missing_watchers = self._config[ 'add_missing_watchers'] logging.info( "add_missing_watchers was set to '%s' from the configuration file" % self._add_missing_watchers) else: self._add_missing_watchers = DEFAULT_ADD_MISSING_WATCHERS logging.info("add_missing_watchers was set to default '%s'" % self._add_missing_watchers) self._jira = None self._jra = None self._initialize() def setProject(self, project): """ :param project: :return: """ self._project = project def setAddMissingWatchers(self, add_missing_watchers): """ :param add_missing_watchers: :return: """ self._add_missing_watchers = add_missing_watchers def _initialize(self): """ :return: """ print("Attempting to connect to JIRA at '%s'" % self._base_url) self._jira = JIRA(self._base_url, basic_auth=(self._username, self._password)) print("Attempting to retrieve info for project '%s'" % self._project) self._jra = self._jira.project(self._project) def getReport(self): """ :return: """ self.report_misc() self.report_components() self.report_roles() self.report_versions() self.report_open_issues() def report_misc(self): """ :return: """ print(Fore.BLUE + "Project name '%s'" % self._jra.name) print(Fore.BLUE + "Project lead '%s'" % self._jra.lead.displayName) print(Style.RESET_ALL) def report_components(self): """ :return: """ components = self._jira.project_components(self._jra) if len(components) > 0: print(Fore.BLUE + "Here are the components") print(Style.RESET_ALL) for c in components: print(c.name) else: print(Fore.RED + "There are no components") print(Style.RESET_ALL) def report_roles(self): """ :return: """ roles = self._jira.project_roles(self._jra) if len(roles) > 0: print(Fore.BLUE + "Here are the roles") print(Style.RESET_ALL) for r in roles: print(r) else: print(Fore.RED + "There are no roles") print(Style.RESET_ALL) def report_versions(self): """ :return: """ versions = self._jira.project_versions(self._jra) if len(versions) > 0: print(Fore.BLUE + "Here are the versions") print(Style.RESET_ALL) for v in reversed(versions): print(v.name) else: print(Fore.RED + "There are no versions") print(Style.RESET_ALL) def report_watchers(self, issue): """ :param issue: :return: """ watcher = self._jira.watchers(issue) print("Issue '%s' has '%d' watcher(s)" % (issue.key, watcher.watchCount)) current_watchers_email = {} for watcher in watcher.watchers: current_watchers_email[watcher.emailAddress] = True print("'%s' - '%s'" % (watcher, watcher.emailAddress)) # watcher is instance of jira.resources.User: # print(watcher.emailAddress) for watcher_email in self._config['members_email_lookup']: if not watcher_email in current_watchers_email: print(Fore.RED + "member '%s' needs to be added as a watcher to '%s'" % (watcher_email, issue.key)) username = self._config['members_email_lookup'][watcher_email] print("Need to add username '%s'" % username) print(Style.RESET_ALL) if self._add_missing_watchers: self._jira.add_watcher(issue, username) print("Exiting") sys.exit(0) print(Style.RESET_ALL) def checkWatchers(self): """ :return: """ issues = self._jira.search_issues('project= LO AND status != Done', maxResults=DEFAULT_MAX_RESULTS) if len(issues) > 0: for issue in issues: self.report_watchers(issue) def report_open_issues(self): issues = self._jira.search_issues('project= LO AND status != Done', maxResults=DEFAULT_MAX_RESULTS) if len(issues) > 0: print(Fore.BLUE + "Found the following '%d' open issues" % len(issues)) print(Style.RESET_ALL) for issue in issues: summary = issue.fields.summary id = issue.id key = issue.key print("id '%s' key '%s' summary : '%s'" % (id, key, summary)) if DEFAULT_REPORT_WATCHERS: self._report_watchers(issue) print(Style.RESET_ALL) def getComments(self, key): """ :param key: :return: """ logging.info("Attempting to retrieve the issue with key '%s'" % key) issues = self._jira.search_issues('key = ' + key) if len(issues) > 1: raise Exception("Expected only one issue for '%s' but found '%d'" % (key, len(issues))) if len(issues) == 1: # comments = issues[0].fields.comment.comments # comments = issues[0].raw['fields']['comment']['comments'] comments = self._jira.comments(issues[0]) if len(comments) > 0: print("Found the following '%d' comments" % len(comments)) comment_ctr = 0 for comment_id in comments: print("-----------------------------------") comment_ctr += 1 comment = self._jira.comment(key, comment_id) author = comment.author.displayName date_created = comment.created body = comment.body print(Fore.BLUE + "%d. author '%s' date '%s'" % (comment_ctr, author, date_created)) print(Style.RESET_ALL) print(body)
class jiramenu(): user = None auth = None config = None debug = False r = Rofi() issues = [] rofi_list = [] def __init__(self, config, debug): self.config = config self.r.status("starting jiramenu") try: self.auth = JIRA(config['JIRA']['url'], basic_auth=(config['JIRA']['user'], config['JIRA']['password'])) except Exception as error: self.r.exit_with_error(str(error)) self.debug = debug def log(self, text): if not self.debug: return print(text) def show(self, user): self.user = user if user: self.log(f"show issues for: {self.user}") query = self.config['JIRA']['query'] if user: query += f" and assignee = {user}" self.log(f"Query: {query}") if not self.issues: self.issues = self.auth.search_issues(query) if not self.rofi_list: if user: self.rofi_list.append(">>ALL") else: self.rofi_list.append(">>MINE") for issue in self.issues: issuetext = '' if issue.fields.assignee: issuetext = f'[{issue.fields.assignee.name}]' if issue.fields.status.id == str(3): #id:3 = Work in Progress issuetext += '{WIP}' issuetext += f'{issue.key}:{issue.fields.summary}' self.rofi_list.append(issuetext) # print active query plus number of results on top index, key = self.r.select(f'{query}[{len(self.rofi_list)}]', self.rofi_list, rofi_args=['-i'], width=100) del key if index < 0: exit(1) if index == 0: self.issues = [] self.rofi_list = [] if user: self.show(None) else: self.show(self.config['JIRA']['user']) return self.show_details(index, user) def addComment(self, ticket_number): comment = self.r.text_entry("Content of the comment:") if comment: # replace @user with [~user] comment = re.sub(r"@(\w+)", r"[~\1]", comment) self.auth.add_comment(ticket_number, comment) def show_details(self, index, user): inputIndex = index ticket_number = re.sub(r"\[.*\]", "", self.rofi_list[index]) ticket_number = re.sub(r"\{.*\}", "", ticket_number) ticket_number = ticket_number.split(":")[0] self.log("[details]" + ticket_number) issue_description = self.issues[index - 1].fields.description output = [] output.append(">>show in browser") output.append("[[status]]") output.append(self.issues[index - 1].fields.status.name) output.append("[[description]]") output.append(issue_description) if self.auth.comments(ticket_number): output.append("[[comments]]") comment_ids = self.auth.comments(ticket_number) for comment_id in comment_ids: self.log("comment_id: " + str(comment_id)) commenttext = '[' + self.auth.comment( ticket_number, comment_id).author.name + ']' commenttext += self.auth.comment(ticket_number, comment_id).body output.append(commenttext) else: output.append("[[no comments]]") output.append(">>add comment") if self.issues[index - 1].fields.assignee: output.append("[[assignee]]" + self.issues[index - 1].fields.assignee.name) else: output.append(">>assign to me") if self.issues[index - 1].fields.status.id == str(3): # WIP output.append(">>in review") else: output.append(">>start progress") output.append('<<back') index, key = self.r.select(ticket_number, output, width=100) if index in [-1, len(output) - 1]: self.show(user) return if index == len(output) - 2: # move issue to 'In Review' self.log("[status]" + self.issues[inputIndex - 1].fields.status.name) self.log("[transitions]") self.log(self.auth.transitions(ticket_number)) if self.issues[inputIndex - 1].fields.status.id == str(3): # WIP for trans in self.auth.transitions(ticket_number): if trans['name'] == "in Review": self.log("move to 'in Review'") self.auth.transition_issue(ticket_number, trans['id']) else: for trans in self.auth.transitions(ticket_number): if trans['name'] == "Start Progress": self.log("move to 'Start Progress'") self.auth.transition_issue(ticket_number, trans['id']) self.show_details(inputIndex, user) return if index == len(output) - 4: # add comment self.log("[addComment]") self.addComment(ticket_number) self.show_details(inputIndex, user) return if index == len(output) - 3: # assign to me self.log("[assign to me]") self.auth.assign_issue(ticket_number, self.config['JIRA']['user']) self.show_details(inputIndex, user) return if index in [3, 4]: Popen(['notify-send', issue_description, '-t', '30000']) self.show_details(inputIndex, user) return # show in browser self.log("[show in browser]") uri = self.auth.issue(ticket_number).permalink() Popen(['nohup', self.config['JIRA']['browser'], uri], stdout=DEVNULL, stderr=DEVNULL)
class JiraClient(object): """ Helper class for the JIRA """ def __init__(self, url, username, password): """ :param url: :param username: :param password: :return: """ self.url = url self.webhook_url = self.url.strip('/') + '/rest/webhooks/1.0/webhook' self.basic_auth = (username, password) self.client = JIRA(url, basic_auth=self.basic_auth) def __str__(self): return '{} {}'.format(self.__class__.__name__, self.url) ################ # Accounts # ################ def groups(self): """ :return: """ return self.client.groups() def users(self, json_result=True): """ :param json_result: :return: """ return self._search_users('_', json_result=json_result) def users_by_email(self, email, json_result=True): """ :param email: :param json_result: :return: """ return self._search_users(email, json_result=json_result) def users_by_group(self, name): """ :param name: :return: """ try: return self.client.group_members(name) except JIRAError as exc: logger.warning(exc) return dict() def _search_users(self, qstr, json_result=True): """ :param qstr: :param json_result: :return: """ def _user_dict_format(user): # Note: Keep the consistent return format with the users_by_group method return {'key': user.key, 'active': user.active, 'fullname': user.displayName, 'email': user.emailAddress } users = self.client.search_users(qstr) if json_result: # Note: Keep the consistent return format with the users_by_group method return [_user_dict_format(user) for user in users] else: return users ############# # Project # ############# def create_project(self, key): """ :param key: :return: """ return self.client.create_project(key) def get_project(self, key, json_result=True): """ :param key: :param json_result: :return: """ if json_result: return self.client.project(key).raw else: return self.client.project(key) def get_projects(self, json_result=True): """ :param json_result: :return: """ project_objs = self.client.projects() if json_result: return [_each.raw for _each in project_objs] else: return project_objs def delete_project(self, key): """ :param key: :return: """ return self.client.delete_project(key) ############# # Version # ############# def get_project_versions(self, name, json_result=True): """ :param name: project name :param json_result: :return: """ try: version_objs = self.client.project_versions(name) if json_result: return [_each.name for _each in version_objs] else: return version_objs except Exception as exc: logger.warn(exc) return [] def create_project_version(self, name, project_name, **kwargs): """ :param name: version name :param project_name: project name :param kwargs: :return: """ return self.client.create_version(name, project_name, **kwargs) ############# # fields # ########### # def get_fields(self): """ :return: """ return self.client.fields() def get_non_custom_fields(self): """ :return: """ return [each for each in self.client.fields() if not each.get('custom', True)] def get_custom_fields(self): """ :return: """ return [each for each in self.client.fields() if each.get('custom', True)] def get_field_id_by_name(self, name): """ :param name: :return: """ ids = [each['id'] for each in self.client.fields() if each.get('name', '') == name] if ids: return ids[0] else: return None def get_field_id_for_hours_left(self): """ :return: """ # For Argo customized field name = 'Hrs Left' return self.get_field_id_by_name(name) ############ # issues # ############ def get_issue(self, name, json_result=True): """ :param name: :param json_result: :return: """ try: issue_obj = self.client.issue(name) except JIRAError as exc: logger.warn('Not found: %s', exc) return None if json_result: issue_dict = copy.deepcopy(issue_obj.raw['fields']) issue_dict['url'] = issue_obj.self issue_dict['id'] = issue_obj.id issue_dict['key'] = issue_obj.key # Replace custom field name return issue_dict else: return issue_obj def add_fix_version_to_issue(self, issue_name, version_name, issuetype=None): """ :param issue_name: :param version_name: :param issuetype: :return: """ return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='fixVersions') def add_affected_version_to_issue(self, issue_name, version_name, issuetype=None): """ :param issue_name: :param version_name: :param issuetype: :return: """ return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='versions') def _add_versions_from_issue(self, issue_name, version_name, issuetype=None, _version_type='fixVersions'): """ :param issue_name: :param version_name: :param issuetype: :param _version_type: :return: """ assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type' issue_obj = self.get_issue(issue_name, json_result=False) if not issue_obj: return None if issuetype is not None and issue_obj.fields.issuetype.name.lower() != issuetype.lower(): logger.info('SKIP. The issue type is %s, expected issue type is %s', issue_obj.fields.issuetype.name.lower(), issuetype.lower()) return None logger.info('Update issue %s, with %s %s', issue_obj.key, _version_type, version_name) ret = issue_obj.add_field_value(_version_type, {'name': version_name}) issue_obj.update() return ret def remove_affected_versions_from_issue(self, issue_name, versions_to_remove): """ :param issue_name: :param versions_to_remove: :return: """ return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='versions') def remove_fix_versions_from_issue(self, issue_name, versions_to_remove): """ :param issue_name: :param versions_to_remove: :return: """ return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='fixVersions') def _remove_versions_from_issue(self, issue_name, versions_to_remove, _version_type='fixVersions'): """ :param issue_name: :param versions_to_remove: :param _version_type: :return: """ assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type' if type(versions_to_remove) not in [list, tuple, set]: versions_to_remove = [versions_to_remove] versions = [] issue_obj = self.get_issue(issue_name, json_result=False) for ver in getattr(issue_obj.fields, _version_type): if ver.name not in versions_to_remove: versions.append({'name': ver.name}) issue_obj.update(fields={_version_type: versions}) def create_issue(self, project, summary, description=None, issuetype='Bug', reporter=None, **kwargs): """ :param project: :param summary: :param description: :param issuetype: :param reporter: :param kwargs: :return: """ # { # "fields": { # "project": # { # "key": "TEST" # }, # "summary": "Always do right. This will gratify some people and astonish the REST.", # "description": "Creating an issue while setting custom field values", # "issuetype": { # "name": "Bug" # }, # "customfield_11050" : {"Value that we're putting into a Free Text Field."} # } # } fields_dict = dict() for k, v in kwargs.items(): fields_dict[k] = v fields_dict['project'] = {'key': project} fields_dict['description'] = description or '' fields_dict['summary'] = summary fields_dict['issuetype'] = {'name': issuetype} if reporter: users = self.users_by_email(reporter, json_result=False) if users: fields_dict['reporter'] = {'name': users[0].name} return self.client.create_issue(fields=fields_dict) def update_issue(self, name, **kwargs): """ :param name: :param kwargs: :return: """ issue_obj = self.get_issue(name, json_result=False) issue_obj.update(**kwargs) ###################### # issue comments # ###################### def get_issue_comments(self, issue_name, latest_num=5, json_result=True): """ :param issue_name: :param latest_num: :param json_result: :return: """ try: comments = self.client.comments(issue_name) except JIRAError as exc: logger.warn(exc) return [] comments = comments[::-1][:latest_num] if json_result: return [each.raw for each in comments] else: return comments def add_issue_comment(self, issue_name, msg, commenter=None): """ :param issue_name: :param msg: :param commenter: :return: """ if commenter: users = self.users_by_email(commenter, json_result=False) if users: msg_header = 'The comment is created by {}({}) from AX system. \n\n'.\ format(users[0].displayName, users[0].emailAddress) msg = msg_header + msg return self.client.add_comment(issue_name, msg) ############### # issue type # ############### def get_issue_types(self, json_result=True): """ :param json_result: :return: """ objs = self.client.issue_types() if json_result: return [obj.raw for obj in objs] else: return objs def get_issue_type_by_name(self, name, json_result=True): """ :param name: :param json_result: :return: """ try: obj = self.client.issue_type_by_name(name) except KeyError as exc: logger.warn(exc) return None else: if json_result: return obj.raw else: return obj ############# # Query # ############# def query_issues(self, **kwargs): """ :param kwargs: :return: max_results: maximum number of issues to return. Total number of results If max_results evaluates as False, it will try to get all issues in batches. json_result: JSON response will be returned when this parameter is set to True. Otherwise, ResultList will be returned """ SUPPORTED_KEYS = ('project', 'status', 'component', 'labels', 'issuetype', 'priority', 'creator', 'assignee', 'reporter', 'fixversion', 'affectedversion') max_results = kwargs.pop('max_results', 100) _json_result = kwargs.pop('json_result', False) jql_str_list = [] for k, v in kwargs.items(): if k not in SUPPORTED_KEYS: continue jql_str_list.append('{} = "{}"'.format(k.strip(), v.strip())) if jql_str_list: jql_str = ' AND '.join(jql_str_list) else: jql_str = '' # Fetch ALL issues try: ret = self.client.search_issues(jql_str, maxResults=max_results, json_result=_json_result) except Exception as exc: logger.warn(exc) ret = {"issues": []} return ret ################ # Query Issues # ################ def get_issues_by_project(self, project_name, **kwargs): """ :param project_name: :param kwargs: :return: """ return self.query_issues(project=project_name, **kwargs) def get_issues_by_component(self, component, **kwargs): """ :param component: :param kwargs: :return: """ return self.query_issues(component=component, **kwargs) def get_issues_by_assignee(self, assignee, **kwargs): """ :param assignee: :param kwargs: :return: """ return self.query_issues(assignee=assignee, **kwargs) def get_issues_by_status(self, status, **kwargs): """ :param status: :param kwargs: :return: """ return self.query_issues(status=status, **kwargs) def get_issues_by_label(self, labels, **kwargs): """ :param labels: :param kwargs: :return: """ return self.query_issues(labels=labels, **kwargs) def get_issues_by_fixversion(self, fix_version, **kwargs): """ :param fix_version: :param kwargs: :return: """ return self.query_issues(fixverion=fix_version, **kwargs) def get_issues_by_affectedversion(self, affected_version, **kwargs): """ :param affected_version: :param kwargs: :return: """ return self.query_issues(affectedversion=affected_version, **kwargs) ################ # Query Hours # ################ def get_total_hours(self, **kwargs): """ :param kwargs: :return: """ all_issues = self.query_issues(**kwargs) field_id = self.get_field_id_for_hours_left() hours = [getattr(iss_obj.fields, field_id) for iss_obj in all_issues] return sum([float(each) for each in hours if each]) def get_total_hours_by_project(self, project_name): """ :param project_name: :return: """ return self.get_total_hours(project=project_name) def get_total_hours_by_component(self, component): """ :param component: :return: """ return self.get_total_hours(component=component) def get_total_hours_by_assignee(self, assignee): """ :param assignee: :return: """ return self.get_total_hours(assignee=assignee) def get_total_hours_by_label(self, label): """ :param label: :return: """ return self.get_total_hours(labels=label) ############## # webhook # ############## def create_ax_webhook(self, url, projects=None): """Create AX Jira webhook :param url: :param projects: :return: """ payload = copy.deepcopy(AX_JIRA_WEBHOOK_PAYLOAD) payload['name'] = payload['name'] + self._get_cluster_name() payload['url'] = url filter_dict = self._generate_project_filter(projects) logger.info('Webhook project filter is: %s', filter_dict) payload.update(filter_dict) return self._requests(self.webhook_url, 'post', data=payload) def get_ax_webhook(self): """Get AX Jira webhook :return: """ response = self._requests(self.webhook_url, 'get') wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name() ax_whs = [wh for wh in response.json() if wh['name'] == wh_name] if not ax_whs: logger.error('Could not get Jira webhook for this cluster: %s, ignore it', wh_name) else: return ax_whs[0] def update_ax_webhook(self, projects=None): """Update AX Jira webhook :param projects: :return: """ ax_wh = self.get_ax_webhook() if ax_wh: filter_dict = self._generate_project_filter(projects) logger.info('Webhook project filter is: %s', filter_dict) logger.info('Update the webhook %s', ax_wh['self']) return self._requests(ax_wh['self'], 'put', data=filter_dict) else: logger.warn('Skip the webhook update') def delete_ax_webhook(self): """Delete AX Jira webhook :return: """ response = self._requests(self.webhook_url, 'get') wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name() ax_whs = [wh for wh in response.json() if wh['name'] == wh_name] for wh in ax_whs: logger.info('Delete webhook %s', wh['self']) self._delete_webhook(url=wh['self']) def get_ax_webhooks(self): """Get all AX webhooks :return: """ response = self._requests(self.webhook_url, 'get') webhooks = response.json() # filter out non-ax webhooks return [wh for wh in webhooks if wh['name'].startswith(AX_JIRA_WEBHOOK_PAYLOAD['name'])] def delete_ax_webhooks(self): """Delete all AX Jira webhooks :return: """ ax_whs = self.get_ax_webhooks() for wh in ax_whs: logger.info('Delete webhook %s', wh['self']) self._delete_webhook(url=wh['self']) def _generate_project_filter(self, projects): """ :param projects: :return: """ if not projects: filter_str = '' else: project_filter_list = [] project_objs = self.get_projects(json_result=False) for pkey in projects: ps = [p for p in project_objs if p.key == pkey] if not ps: logger.error('Could not get project %s, ignore it', pkey) else: project_filter_list.append('Project = {}'.format(ps[0].name)) filter_str = ' AND '.join(project_filter_list) return {'filters': {'issue-related-events-section': filter_str } } def _delete_webhook(self, url=None, id=None): """Delete webhook :param url: :param id: :return: """ if url is None: url = self.webhook_url + '/' + str(id) return self._requests(url, 'delete') def _get_cluster_name(self): """ :return: """ return os.environ.get('AX_CLUSTER', UNKNOWN_CLUSTER) def _requests(self, url, method, data=None, headers=None, auth=None, raise_exception=True, timeout=30): """ :param url: :param method: :param data: :param headers: :param auth: :param raise_exception: :param timeout: :return: """ headers = {'Content-Type': 'application/json'} if headers is None else headers auth = self.basic_auth if auth is None else auth try: response = requests.request(method, url, data=json.dumps(data), headers=headers, auth=auth, timeout=timeout) except requests.exceptions.RequestException as exc: logger.error('Unexpected exception occurred during request: %s', exc) raise logger.debug('Response status: %s (%s %s)', response.status_code, response.request.method, response.url) # Raise exception if status code indicates a failure if response.status_code >= 400: logger.error('Request failed (status: %s, reason: %s)', response.status_code, response.text) if raise_exception: response.raise_for_status() return response
class TestIssues(unittest.TestCase): def setUp(self): self.jira = JIRA(options=dict(server=TEST_URL, verify=False), basic_auth=(TEST_USERNAME, TEST_PASSWORD)) self.issue1 = self.jira.create_issue( project='KB', summary='Test-1', issuetype={'name': 'Bug'}, ) self.issue2 = self.jira.create_issue( project='KB', summary='Test-2', issuetype={'name': 'Bug'}, ) def tearDown(self): issues = self.jira.search_issues( 'project = "KB" AND summary ~ "Test*"', fields=['key']) for _ in issues: _.delete() def assert_single_attachment(self): # TODO - Find how to test this automatically pass def assert_single_comment_with(self, text): comments = self.jira.comments(self.issue1.key) self.assertEqual(len(comments), 1) self.assertIn(text, comments[0].body) def test_new(self): result = CliRunner().invoke(topcli, ['issue', 'new', 'KB', 'task', 'Test-new']) self.assertEqual(result.exit_code, 0) issues = self.jira.search_issues( 'project = "KB" AND summary ~ "Test-new"', fields=['key', 'summary']) self.assertEqual(len(issues), 1) self.assertIn(issues[0].key, result.output) def test_transition(self): result = CliRunner().invoke( topcli, ['issue', 'transition', self.issue1.key, 'Done']) self.assertEqual(result.exit_code, 0) def test_assign(self): result = CliRunner().invoke( topcli, ['issue', 'assign', self.issue1.key, TEST_USERNAME]) self.assertEqual(result.exit_code, 0) assignee = self.jira.issue(self.issue1.key, fields=['assignee']).fields.assignee self.assertEqual(assignee.key, TEST_USERNAME) def test_unassign(self): result = CliRunner().invoke( topcli, ['issue', 'assign', self.issue1.key, TEST_USERNAME]) result = CliRunner().invoke(topcli, ['issue', 'unassign', self.issue1.key]) self.assertEqual(result.exit_code, 0) assignee = self.jira.issue(self.issue1.key, fields=['assignee']).fields.assignee self.assertIsNone(assignee) def test_attach_file(self): with CliRunner().isolated_filesystem() as dir_path: with open('data.txt', 'w') as f: print('abc', file=f) result = CliRunner().invoke( topcli, ['issue', 'attach', self.issue1.key, 'data.txt']) self.assertEqual(result.exit_code, 0) self.assert_single_attachment() def test_comment_args(self): result = CliRunner().invoke( topcli, ['issue', 'comment', self.issue1.key, 'Comment', 'from args']) self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from args') def test_comment_file(self): with CliRunner().isolated_filesystem() as dir_path: with open('comment.txt', 'w') as f: print('Comment from file', file=f) result = CliRunner().invoke( topcli, ['issue', 'comment', self.issue1.key, 'comment.txt']) self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from file') def test_comment_prompt(self): result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key], input='Comment from prompt\n') self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment from prompt') def test_comment_stdin(self): result = CliRunner().invoke(topcli, ['issue', 'comment', self.issue1.key, '-'], input='Comment\nfrom\nstdin') self.assertEqual(result.exit_code, 0) self.assert_single_comment_with('Comment\nfrom\nstdin') def test_link(self): result = CliRunner().invoke(topcli, [ 'issue', 'link', self.issue1.key, self.issue2.key, '-t', 'duplicates' ]) self.assertEqual(result.exit_code, 0) links = self.jira.issue(self.issue1.key, fields=['issuelinks']).fields.issuelinks self.assertEqual(len(links), 1) self.assertEqual(links[0].outwardIssue.key, self.issue2.key) self.assertEqual(links[0].type.outward, 'duplicates') def test_unlink(self): result = CliRunner().invoke(topcli, [ 'issue', 'link', self.issue1.key, self.issue2.key, '-t', 'duplicates' ]) self.assertEqual(result.exit_code, 0) result = CliRunner().invoke( topcli, ['issue', 'unlink', self.issue1.key, self.issue2.key]) links = self.jira.issue(self.issue1.key, fields=['issuelinks']).fields.issuelinks self.assertEqual(len(links), 0) def test_search_issue(self): result = CliRunner().invoke(topcli, ['issue', 'search']) self.assertEqual(result.exit_code, 0) self.assertIn('KB-1', result.output) self.assertIn('KB-2', result.output) self.assertIn('KB-3', result.output)
class Jira(): def __init__(self): options = { 'verify': False, 'server': 'https://jira.test.com' } # Options to connect to JIRA WORK_DIR = os.path.dirname(os.path.realpath(__file__)) CREDENTIAL_FILE = 'credentials.json' with open(WORK_DIR + '/' + CREDENTIAL_FILE, 'r') as file: credential = json.load(file) self.jira = JIRA(options, basic_auth=(credential["jira"]["username"], credential["jira"]['password'])) self.user_name = credential["jira"]["username"] self.user_pass = credential["jira"]['password'] self.jira_filter = self.jira.search_issues( 'project="TEAMNAME: Platform Operations" and reporter=nagios and resolution=Unresolved', maxResults=1000) self.jira_filter_for_not_updated = self.jira.search_issues( 'project = TEAMNAME AND resolution = Unresolved AND updated <= -1w AND assignee in (nagios)', maxResults=1000) def assign_and_investigate(self, issue): url = 'https://jira.test.com/rest/api/2/issue/%s/transitions' % issue.key auth = base64.encodestring( '%s:%s' % (self.user_name, self.user_pass)).replace('\n', '') data = json.dumps({'transition': {'id': 11}}) request = urllib2.Request( url, data, { 'Authorization': 'Basic %s' % auth, 'Content-Type': 'application/json', }) print urllib2.urlopen(request).read() def resolve_ticket(self, issue): url = 'https://jira.test.com/rest/api/2/issue/%s/transitions' % issue.key auth = base64.encodestring( '%s:%s' % (self.user_name, self.user_pass)).replace('\n', '') issue.fields.labels.append(u'auto_closed') issue.update(fields={"labels": issue.fields.labels}) data = json.dumps({ 'transition': { 'id': 71 }, 'fields': { 'resolution': { "name": "Self Corrected" } } }) request = urllib2.Request( url, data, { 'Authorization': 'Basic %s' % auth, 'Content-Type': 'application/json', }) print urllib2.urlopen(request).read() def get_status_by_comment(self, issue): if len(self.jira.comments(issue)) > 0: last_comment = "" for comment in self.jira.comments(issue): if str(comment.author) == 'Monitoring': last_comment = comment.body else: return False status = last_comment.split("*")[6].split()[0] if status == "RECOVERY": return True else: return False else: return False
class JiraTool(object): def __init__(self, server, username, password, maxResults=50): self.server = server self.basic_auth = (username, password) # issues查询的最大值 self.maxResults = maxResults def login(self): self.jira = JIRA(server=self.server, basic_auth=self.basic_auth) if self.jira == None: print('连接失败') sys.exit(-1) def get_projects(self): """ 获得jira 的所有项目 :return: """ return [(p.key, p.name, p.id) for p in self.jira.projects()] # def get_components(self, project): # """ # 获得某项目的所有模块 # :param project: # :return: # """ # return [(c.name, c.id) for c in self.jira.project_components(self.jira.project(project))] def create_component(self, project, compoment, description, leadUserName=None, assigneeType=None, isAssigneeTypeValid=False): """ # 创建项目模块 :param project: 模块所属项目 :param compoment:模块名称 :param description:模块描述 :param leadUserName: :param assigneeType: :param isAssigneeTypeValid: :return: """ components = self.jira.project_components(self.jira.project(project)) if compoment not in [c.name for c in components]: self.jira.create_component(compoment, project, description=description, leadUserName=leadUserName, assigneeType=assigneeType, isAssigneeTypeValid=isAssigneeTypeValid) def create_issue(self, project, compoment, summary, description, assignee, issuetype, priority='Medium'): """ 创建提交issue :param project: 项目 :param issuetype: 问题类型,Task :param summary: 主题 :param compoment: 模块 :param description: 描述 :param assignee: 经办人 :param priority: 优先级 :return: """ issue_dict = { 'project': { 'key': project }, 'issuetype': { 'id': issuetype }, 'summary': summary, 'components': [{ 'name': compoment }], 'description': description, 'assignee': { 'name': assignee }, 'priority': { 'name': priority }, } return self.jira.create_issue(issue_dict) def delete_issue(self, issue): """ 删除issue :param issue: :return: """ issue.delete() def update_issue_content(self, issue, issue_dict): """ 更新issue内容 :param issue: :param issue_dict: issue_dict = { 'project': {'key': project}, 'issuetype': {'id': issuetype}, 'summary': summary, 'components': [{'name': compoment}], 'description': description, 'assignee': {'name': assignee}, 'priority': {'name': priority}, } :return: """ issue.update(fields=issue_dict) def update_issue_issuetype(self, issue, issuetype): """ 更新bug 状态 :param issue: :param issuetype: 可以为id值如11,可以为值如'恢复开启问题' :return: """ transitions = self.jira.transitions(issue) # print([(t['id'], t['name']) for t in transitions]) self.jira.transition_issue(issue, issuetype) def search_all_issue(self, jql): block_size = 100 block_num = 0 issues = [] while True: start_idx = block_num * block_size part_issues = self.jira.search_issues(jql, start_idx, block_size) if len(part_issues) == 0: break block_num += 1 issues.extend(part_issues) return issues # def search_issues(self, jql): # """ # 查询bug # :param jql: 查询语句,如"project=项目key AND component = 模块 AND status=closed AND summary ~标题 AND description ~描述" # :return: # """ # try: # # maxResults参数是设置返回数据的最大值,默认是50。 # issues = self.jira.search_issues(jql, maxResults=self.maxResults) # except Exception as e: # print(e) # sys.exit(-1) # return issues def search_issue_content(self, issue, content_type): """ 获取issue 的相关信息 :param issue: :param content_type:项目project; 模块名称components; 标题summary; 缺陷类型issuetype; 具体描述内容description; 经办人assignee; 报告人reporter; 解决结果resolution; bug状态status; 优先级priority; 创建时间created; 更新时间updated; 评论comments :return: """ # 评论 if content_type == 'comments': return [c.body for c in self.jira.comments(issue)] if hasattr(issue.fields, content_type): result = getattr(issue.fields, content_type) if isinstance(result, list): return [c.name for c in result if hasattr(c, 'name')] return result def get_issue_types(self): """ 获取所有issues类型 :return: """ issue_type_name = [] issue_types = self.jira.issue_types() for issue_type in issue_types: issue_type_name.append(issue_type.name) return issue_type_name def get_components(self, issue): """ 获取组件字段 :param issue: 每个issue :return: """ for i in issue.fields.components: components = i.name return components def get_epic_link(self, issue): """ 获取epic_link字段 :param issue: 每个issue :return: """ for i in issue.fields.customfield_10900: epic_link = i.name return epic_link def make_cse_env_num(self, cse_num): try: res = cse_num.split('-') num = int(res[1]) + 1 cse_env_nu = 'CSE-' + str(num) except Exception as e: cse_env_nu = '' return cse_env_nu def get_result(self, issue): """ 获取处理结果的字段 如 产品问题、环境问题 :param issue: :return: """ if issue.fields.customfield_11100: result = issue.fields.customfield_11100.value else: result = '' return result def get_resolve_time(self, issue): try: resolve_time = issue.fields.resolutiondate[0:19] except Exception as e: print(e) resolve_time = '' print(resolve_time) return resolve_time
class JiraNavigate: """ Wraps the JIRA module to easily obtain information on a project. The underlying communication requires login details and a REST interface. """ def __init__(self): self.jira = JIRA(basic_auth=(session["username"], base64.b64decode(session["password"])), options={'server': session["server"]}) def get_issues(self, jql): logging.debug("JQL: %s" % jql) return self.jira.search_issues(jql) def get_changelog(self, issue): issue = self.jira.issue(self.get_key(issue), expand='changelog') return issue.changelog def get_resolution_date(self, issue): resolution_date = issue.fields.resolutiondate if resolution_date is not None: return self.to_datetime(resolution_date) return None def get_comments(self, issue): return self.jira.comments(issue) @staticmethod def get_title(issue): return "[%s] %s" % (issue.key, issue.fields.summary) @staticmethod def get_assignee(issue): if issue.fields.assignee is None: return "" else: return issue.fields.assignee.displayName @staticmethod def get_key(issue): return issue.key @staticmethod def get_summary(issue): return issue.fields.summary @staticmethod def get_components(issue): return issue.fields.components def get_created_date(self, issue): return self.to_datetime(issue.fields.created) def get_updated_date(self, issue): return self.to_datetime(issue.fields.updated) @staticmethod def get_labels(issue): return issue.fields.labels @staticmethod def get_story_points(issue): story_points = issue.fields.customfield_10103 if story_points > 0: return story_points return 0 def rerank(self, sorted_backlog): for pair in zip(sorted_backlog, sorted_backlog[1:]): key1 = self.get_key(pair[0]["issue"]) key2 = self.get_key(pair[1]["issue"]) self.jira.rank(key2, key1) def count_story_points(self, issues): points = 0 for issue in issues: points += self.get_story_points(issue) return points def sprint_dates(self, sprint_id): def to_date(date): months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"] p = re.match(r"(\d+)/(\w+)/(\d+)", date) year = int(p.group(3))+2000 month = months.index(p.group(2).lower())+1 day = int(p.group(1)) logging.debug("Deconstructed date for {}: {:4d}-{:2d}-{:2d}".format(date, year, month, day)) return datetime.datetime(year, month, day) sprint = self.jira.sprint(sprint_id) dates = dict( start=to_date(sprint.startDate), end=to_date(sprint.endDate) ) logging.debug("{} to {}".format(dates["start"], dates["end"])) return dates def sprint_name(self, sprint_id): return self.jira.sprint(sprint_id) def get_sprint_committed_issues(self, sprint_id, board_name): jql = "Sprint = {:d} AND issueFunction not in " \ "addedAfterSprintStart(\"{}\", \"{}\")".format(sprint_id, board_name, self.sprint_name(sprint_id)) logging.debug("JQL sprint_committed_points: {}".format(jql)) return self.jira.search_issues(jql) def sprint_committed_points(self, sprint_id, board_name): committed_issues = self.get_sprint_committed_issues(sprint_id, board_name) return self.count_story_points(committed_issues) def sprint_completed_points(self, sprint_id, board_name): jql = "issueFunction in completeInSprint(\"{}\", \"{}\")".format(board_name, self.sprint_name(sprint_id)) logging.debug("JQL sprint_completed_points: {}".format(jql)) completed_issues = self.jira.search_issues(jql) return self.count_story_points(completed_issues) def sprint_interrupt_points(self, sprint_id, board_name): jql = "(issueFunction in addedAfterSprintStart(\"{}\", \"{}\"))" \ "AND resolution = Done ".format(board_name, self.jira.sprint(sprint_id)) logging.debug("JQL sprint_interrupt_points: {}".format(jql)) interrupt_issues = self.jira.search_issues(jql) return self.count_story_points(interrupt_issues) def sprint_incomplete_points(self, sprint_id, board_name): jql = "(Sprint = {:d} AND issueFunction not in addedAfterSprintStart(\"{}\", \"{}\")) " \ "AND resolution = Unresolved ".format(sprint_id, board_name, self.sprint_name(sprint_id)) logging.debug("JQL sprint_incomplete_points: %s" % jql) incomplete_issues = self.jira.search_issues(jql) return self.count_story_points(incomplete_issues) def committed_points_timeline(self, sprint_id, board_name): self.sprint_dates(sprint_id) committed_issues = self.get_sprint_committed_issues(sprint_id, board_name) resolved_on_date = {} for issue in committed_issues: resolution_date = self.get_resolution_date(issue) if resolution_date is not None: resolution_date_str = datetime.datetime.strftime(resolution_date, "%Y-%m-%d") points = self.get_story_points(issue) if resolution_date_str in resolved_on_date: resolved_on_date[resolution_date_str] += points else: resolved_on_date[resolution_date_str] = points return resolved_on_date def total_points_timeline(self, sprint_id, board_name): @staticmethod def to_datetime(str_date): """ Takes the text-based timestamp from the JSON document and converts it to a Python datetime. :param str_date: The string version of the date :return: a valid Python datetime object """ p = re.compile("(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.\d+\+(\d+)") m = p.match(str_date) return datetime.datetime( int(m.group(1)), int(m.group(2)), int(m.group(3)), (int(m.group(4)) + int(m.group(7))) % 24, int(m.group(5)), int(m.group(6)) )
class JiraTicket: def __init__(self): #this list is where you extend, don't touch anything else self.employees = [ employee1, employee2 #extend here ] self.new_tickets = {} self.old_tickets = {} self.closed_tickets = {} self.updated_tickets = {} self.needs_update = {} jira_options = {'server': server, 'verify': False} self.jira = JIRA(options=jira_options, basic_auth=('username', 'password')) def get_unix_timestamp(self, issue): split = issue.fields.updated.split('T') split1 = split[1].split('.') created = split[0] + " " + split1[0] return (datetime.strptime(created, '%Y-%m-%d %H:%M:%S')).strftime('%s') #unix_t = timestamp.strftime('%s') return int(unix_t) def get_new_tickets(self, username): JQL = "reporter=" + username + " and status in ('In Progress', open, reopened)" + " order by created desc" JQL_query = self.jira.search_issues(JQL, maxResults=50) for item in JQL_query: self.new_tickets[str(item)] = {} #TODO: don't need this issue = self.jira.issue(str(item)) comments = self.jira.comments(str(item)) self.new_tickets[str(item)] = { 'commentCount': len(issue.fields.comment.comments), 'timestamp': int(datetime.now().strftime('%s')), 'latestCommentAuthor': None } if self.new_tickets[str(item)]['commentCount'] > 0: self.new_tickets[str(item)]['latestCommentAuthor'] = comments[ -1].raw['updateAuthor']['displayName'] else: self.new_tickets[str(item)]['latestCommentAuthor'] = str( issue.fields.reporter) def write_dict_to_file(self, dict): pickle.dump(dict, open("old_jira_tickets.p", "wb")) pickle.dump(dict, open("backup_old_jira_tickets.p", "wb")) def load_dict_from_file(self): try: self.old_tickets = pickle.load(open("old_jira_tickets.p", "rb")) except: self.old_tickets = pickle.load( open("backup_old_jira_tickets.p", "rb")) def compare_old_to_new(self): to_delete_from_old = [] to_delete_from_new = [] for key in self.old_tickets: old_issue = self.old_tickets[key] #check for updated tickets if key in self.new_tickets.keys(): if old_issue['commentCount'] == self.new_tickets[key][ 'commentCount']: if (int(datetime.now().strftime('%s')) - int(old_issue['timestamp'])) > 7 * 24 * 60 * 60: self.needs_update[key] = {} old_issue['timestamp'] = self.new_tickets[key][ 'timestamp'] if old_issue['commentCount'] > 0: self.needs_update[key] = old_issue[ 'latestCommentAuthor'] #else it's been updated else: self.updated_tickets[key] = {} if old_issue['commentCount'] > 0: self.updated_tickets[key] = old_issue[ 'latestCommentAuthor'] old_issue['commentCount'] = self.new_tickets[key][ 'commentCount'] old_issue['timestamp'] = self.new_tickets[key]['timestamp'] to_delete_from_new.append(key) #else it's closed else: self.closed_tickets[key] = {} self.closed_tickets[key] = old_issue['latestCommentAuthor'] to_delete_from_old.append(key) for key in to_delete_from_old: del (self.old_tickets[key]) for key in to_delete_from_new: del (self.new_tickets[key]) #at this point the only tickets left in new_tickets are newly created -> add them to #old_tickets and recently updated for key in self.new_tickets.keys(): ticket = self.new_tickets[key] self.old_tickets[key] = {} self.old_tickets[key]['commentCount'] = ticket['commentCount'] self.old_tickets[key]['timestamp'] = ticket['timestamp'] self.old_tickets[key]['latestCommentAuthor'] = ticket[ 'latestCommentAuthor'] self.updated_tickets[key] = {} self.updated_tickets[key] = ticket['latestCommentAuthor'] def test(self): jt.old_tickets['ticket']['commentCount'] = 5 #create updated ticket jt.old_tickets['ticket'][ 'timestamp'] -= 8 * 24 * 60 * 60 #create needs update ticket del (jt.new_tickets['ticket']) #create closed ticket def print_all_lists(self): print "new tickets" for key in self.new_tickets.keys(): print key print "\nold tickets" for key in self.old_tickets.keys(): print key print "\nupdated tickets" for key in self.updated_tickets.keys(): print key print "\nclosed tickets" for key in self.closed_tickets.keys(): print key # print "\nneeds update tickets" # for key in self.needs_update.keys(): # print key print "\n"
}, 'assignee': { 'name': issue.fields.assignee.name } if issue.fields.assignee else None, 'description': issue.fields.description, 'priority': { 'id': issue.fields.priority.id }, 'issuetype': { 'name': issue.fields.issuetype.name }, 'comments': None } comment_list = [] comments = jira_instance.comments(issue) for comment in comments: comment_list.append({ 'body': comment.body, 'author': comment.author.name, 'created': comment.created }) issue_dict['comments'] = comment_list project_data.append(issue_dict) with open(filename, 'w') as outfile: outfile.write(yaml.dump(project_data, default_flow_style=False)) elif operation == "import": with open(filename, 'r') as data_file: project_data = yaml.load(data_file) for issue_dict in project_data:
class jiramenu(): user = None project = None auth = None config = None debug = False r = Rofi() issues = [] rofi_list = [] def __init__(self, config, debug): self.config = config self.r.status("starting jiramenu") try: self.auth = JIRA(config['JIRA']['url'], basic_auth=(config['JIRA']['user'], config['JIRA']['password'])) except Exception as error: self.r.exit_with_error(str(error)) self.debug = debug def log(self, text): if not self.debug: return print(text) def show(self, user): self.user = user self.project = self.config['JIRA']['project'] if user: self.log(f"show issues for: {self.user}") query = self.config['JIRA']['query'] if user: query += f" and assignee = '{user}'" if self.project: query += f" and project = '{self.project}'" self.log(f"Query: {query}") if not self.issues: self.issues = self.auth.search_issues(query) self.boards = self.auth.boards() if not self.rofi_list: if user: self.rofi_list.append("> all") else: self.rofi_list.append("> mine") self.issues.sort(key=lambda x: x.fields.status.id, reverse=False) for issue in self.issues: labels = '' if len(issue.fields.labels): labels = '(' for idx, label in enumerate(issue.fields.labels): labels += label if idx != len(issue.fields.labels) -1: labels += ', ' labels += ')' issuetext = '' issueassignee = '' initials = ' ' if issue.fields.assignee: issueassignee = issue.fields.assignee.displayName initials = ''.join([x[0].upper() for x in issueassignee.split(' ')]) if issue.fields.status.id == str(3): #id:3 = Work in Progress issuetext = '{WIP}' issuekey = issue.key issuekey = "{:<9}".format(issuekey) status = "{:<24}".format(issue.fields.status.name) issueassignee = "{:<20}".format(issueassignee) issuetext += f'{issuekey} {status} {initials} {labels} {issue.fields.summary}' self.rofi_list.append(issuetext) # print active query plus number of results on top index, key = self.r.select(f'{query}[{len(self.rofi_list)}]', self.rofi_list, rofi_args=['-i'], width=100) del key if index < 0: exit(1) if index == 0: self.issues = [] self.rofi_list = [] if user: self.show(None) else: self.show(self.config['JIRA']['user']) return self.show_details(index, user) def addComment(self, ticket_number): comment = self.r.text_entry("Content of the comment:") if comment: # replace @user with [~user] comment = re.sub(r"@(\w+)", r"[~\1]", comment) self.auth.add_comment(ticket_number, comment) def show_details(self, index, user): inputIndex = index # ticket_number = re.match("IMP-([1-9]|[1-9][0-9])+", self.rofi_list[index]).group(0) issue = self.issues[index-1] ticket_number = issue.key summary = '-'.join(issue.fields.summary.split(' ')) branch_name= ticket_number + '-' + summary[:33] self.log("[details]" + ticket_number) issue_description = issue.fields.description output = [] output.append("> show in browser") output.append("") output.append(f"> copy branch ({branch_name})") output.append("") output.append("Status: " + self.issues[index - 1].fields.status.name) # output.append("Description: " + issue_description) description = [] if issue_description: description = issue_description.split('\n') for item in description: output.append(item) if self.auth.comments(ticket_number): comment_ids = self.auth.comments(ticket_number) for comment_id in comment_ids: self.log("comment_id: " + str(comment_id)) commentauthor = self.auth.comment(ticket_number, comment_id).author.displayName + ':' output.append(commentauthor) commenttext = self.auth.comment(ticket_number, comment_id).body commenttext = commenttext.split('\n') for line in commenttext: output.append(line) else: output.append("no comments") output.append("") output.append("> add comment") output.append("") if self.issues[index - 1].fields.assignee: output.append("assigned to: " + self.issues[index - 1].fields.assignee.displayName) else: output.append("> assign to me") # if self.issues[index - 1].fields.status.id == str(3): # WIP # output.append(">>in review") # else: # output.append(">>start progress") output.append("") output.append('< back') index, key = self.r.select(ticket_number, output, width=100) if index in [-1, len(output) - 1]: self.show(user) return # if index == len(output) - 2: # move issue to 'In Review' # self.log("[status]"+self.issues[inputIndex - 1].fields.status.name) # self.log("[transitions]") # self.log(self.auth.transitions(ticket_number)) # if self.issues[inputIndex - 1].fields.status.id == str(3): # WIP # for trans in self.auth.transitions(ticket_number): # if trans['name'] == "in Review": # self.log("move to 'in Review'") # self.auth.transition_issue(ticket_number, trans['id']) # # else: # for trans in self.auth.transitions(ticket_number): # if trans['name'] == "Start Progress": # self.log("move to 'Start Progress'") # self.auth.transition_issue(ticket_number, trans['id']) # self.show_details(inputIndex, user) # return if index == len(output) - 4: # add comment self.log("[addComment]") self.addComment(ticket_number) self.show_details(inputIndex, user) return if index == len(output) - 3: # assign to me self.log("[assign to me]") self.auth.assign_issue(ticket_number, self.config['JIRA']['user']) self.show_details(inputIndex, user) return if index == 2: pyperclip.copy(branch_name) return # if index in [3, 4]: # Popen(['notify-send', issue_description, '-t', '30000']) # self.show_details(inputIndex, user) # return # show in browser self.log("[show in browser]") uri = self.auth.issue(ticket_number).permalink() Popen(['nohup', self.config['JIRA']['browser'], uri], stdout=DEVNULL, stderr=DEVNULL)
ticketCount += 1 print '(' + str(ticketCount) + '/' + str( numberOfTicketsToResolve) + ')[' + str( ticketResolveCount ) + ']resolved\nNow working on... ' + issue.key jiraBeforeMessage = "This is an automated message.\n\n" jiraStartMessage += "==========VIRUS TOTAL WAS ABLE TO FIND THE FOLLOWING URLs==========\n\n" jiraEndMessage = '' URL_List = list() Email_list = list() textField = issue.fields.description textField = textField.replace('|', ' ') URL_List.extend(get_all_URLS_in_text(textField)) comments = jira.comments(issue.key) skipIssue = False for comment in comments: #print comment.author.name if comment.author.name == 'ltang': skipIssue = True break if skipIssue == True: print '\nalready commented for this issue - skipping\n' numberOfTicketsToResolve -= 1 ticketCount -= 1 continue try: attachments = issue.fields.attachment for attachment in attachments:
class JiraManager(): def __init__(self): self.jira_options = {'server': 'https://jira.omnichat.tech/'} self.jira = JIRA(options=self.jira_options, auth=('a.balov', 'MBcLySH5')) self.df_to_check = pd.DataFrame() self.df_to_score = pd.DataFrame() self.minimal_scores = 12 self.bot = TelegramBot1() self.engine = create_engine('postgresql+psycopg2://postgres:[email protected]:5432/jetforms_src') self.con = psycopg2.connect(database="jetforms_src", user="******", password="******", host="172.19.16.130", port="5432") self.df_scores = pd.read_sql('select * from jira_scoretable', self.con) self.df_ignorelist = pd.read_sql('select * from jira_excepted_users', self.con) def _adress_security_checker(self, adress): rtk = ['ROSTELECOM', r'*****@*****.**', 'rostelecom-cc.ru', '*****@*****.**',r'@sibir.rt.ru','*****@*****.**', r'@dv.rt.ru', r'@nw.rt.ru','@mail.ntt.ru','@rt.ru', '@RT.RU','@center.rt.ru','@volga.rt.ru','@south.rt.ru'] for i in rtk: if i in adress: return True return False def get_mail(self): df = pd.DataFrame() outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") inbox = outlook.GetDefaultFolder(6) for item in inbox.Items: if item.SenderEmailType == 'EX': task_customer = item.Sender.GetExchangeUser().PrimarySmtpAddress else: task_customer = item.SenderEmailAddress if self._adress_security_checker(task_customer): try: if self.attribute_mail_checker(who=task_customer, whom=item.To, theme=self.beautiful_theme(item.subject), text = item.body.split("с уважением")[0]): df = df.append({'text': item.body, 'subj': self.beautiful_theme(item.subject), 'cust': task_customer}, ignore_index=True) except: print('') item.delete() self.df_to_check = self.df_to_check.append(df) def beautiful_theme(self, theme): return theme.replace("FW: ", "").replace("FW:", "").replace("RE: ", "").replace("RE:", "") #first checking step: customer and mail-adress To def attribute_mail_checker(self, who='', whom='', theme='', text=''): if '*****@*****.**' in whom: for item in self.df_ignorelist['mail']: if who == item: return False else: return False atr = self.check_task_from_db(self.beautiful_theme(theme)) if len(atr) != 0: self.add_comment_to_issue(str(atr[0]), '#S'+text.split('С уважением')[0]) return False return True #second checking step: semantic points def semantic_manager(self): def splitter(text): text = text.lower() text = text.split("с уважением") text = text[0].replace('_x000d_', '') text[0].replace(',', '') text[0].replace('.', '') text[0].replace('\n', ' ') return text.split(' ') scoretable = {} for ind in self.df_scores.index.values: scoretable.update({self.df_scores['text'].iloc[ind]: self.df_scores['score'].iloc[ind]}) for ind2 in self.df_to_check.index.values: item = self.df_to_check['text'].iloc[ind2] scores = 0 for word in splitter(item): parsed_str = word.replace('\n', '').replace('.', '').replace(',', '') try: scores += scoretable[parsed_str] except: scores += 0 if scores >= self.minimal_scores: self.df_to_score = self.df_to_score.append(self.df_to_check.iloc[ind2]) def create_jira_issue(self, issue_dict, customer): new_issue = self.jira.create_issue(fields=issue_dict) self.bot.send_group_message('Вам заявочка пришла, '+shiz(str(new_issue))+': '+issue_dict['summary']+'\n'+customer+' пишет: '+str(new_issue.fields.description)) return str(new_issue) def send_mail_to_user(self, customer, text, mail_theme, att = ''): i = Mail(to = customer, copy = '*****@*****.**', theme = mail_theme, body = text, attach = att ) i.send_mail() def search_jira_issue(self, id): issue = self.jira.issue(id) res = {'ID': str(issue), 'issuetype': issue.fields.issuetype.name, 'status': issue.fields.status.name, 'summary': issue.fields.summary, 'description': issue.fields.status.description, 'created': issue.fields.created[:10], 'resolutiondate': str(issue.fields.resolutiondate)[:10], 'assignee': str(issue.fields.assignee)} return res def add_task_to_db(self, customer, jira_att): jira_att.update({'customer': customer}) df = pd.DataFrame(jira_att, index=[0]) df.to_sql('jira_archive', self.engine, schema ='public', if_exists="append", index = False) def get_jira_updates(self): df_archive = pd.read_sql('''select * from jira_archive where status <> 'Done' and status <> 'Закрыто' ''', self.con) df_archive['created'] = pd.to_datetime(df_archive['created']).apply(lambda x: str(x.date())) for ind in df_archive.index.values: try: df_archive['resolutiondate'].iloc[ind] = str(df_archive['resolutiondate'].iloc[ind].date()) except: pass issue = self.jira.issue(df_archive['ID'].iloc[ind]) customer = df_archive['customer'].iloc[ind] jira_dict = {'ID': str(issue), 'issuetype': issue.fields.issuetype.name, 'status': issue.fields.status.name, 'summary': issue.fields.summary, 'description': issue.fields.status.description, 'created': issue.fields.created[:10], 'resolutiondate': str(issue.fields.resolutiondate)[:10], 'assignee': str(issue.fields.assignee)} jira_dict.update({'customer': customer}) jira_dict.update({'comments': self.get_comments(jira_dict['ID'])}) df_to_dict = df_archive.iloc[ind].to_dict() if jira_dict['resolutiondate'] != df_to_dict['resolutiondate'] or jira_dict['comments'] != df_to_dict['comments']: q = '''DELETE FROM jira_archive WHERE "ID" = '%s' '''% df_archive['ID'].iloc[ind] cur = self.con.cursor() cur.execute(q) self.con.commit() to_add = pd.DataFrame(jira_dict, index =[0]) to_add.to_sql('jira_archive', self.engine, schema ='public', if_exists="append", index = False) self.send_mail_to_user(jira_dict['customer'], 'Обновление по заявке: ' + jira_dict['ID'] + '\n'+ self.beautiful_body(jira_dict)+ '\n\n', jira_dict['summary'], self.get_attach_from_jira(jira_dict['ID'])) def check_task_from_db(self, theme): df_search = pd.read_sql('''select "ID" from jira_archive where status <> 'Done' AND summary ='%s' ''' %theme, self.con) return(df_search['ID']) def add_comment_to_issue(self, id, text): self.jira.add_comment(str(id), 'Пользователь пишет: '+text) self.bot.send_group_message('Пользователь пишет: , '+ text +'\nЗаявочка: '+shiz(str(id))) def update_sources(self): self.df_scores = pd.read_sql('select * from jira_scoretable', self.con) self.df_ignorelist = pd.read_sql('select * from jira_excepted_users', self.con) def get_comments(self, id): comment_text = '' issue = self.jira.issue(id) comments = self.jira.comments(issue) for comment in comments: if '#S' in str(comment.body): comment_text += str(comment.author)+ ': '+str(comment.body) + '\n' return comment_text.replace('Балов Александр: Пользователь пишет','Пользователь пишет') def look_comments_from_jira(self, id): issue = self.jira.issue(id) att_list = [] comments = self.jira.comments(issue) for comment in comments: att_list.append(str(comment.body).replace('[','').replace(']','').replace('!','').replace('^','').replace(']','')) return att_list def get_attach_from_jira(self, id): issue = self.jira.issue(id) att_list = [] true_list = self.look_comments_from_jira(id) for attachment in issue.fields.attachment: image = attachment.get() filename=attachment.filename if filename in true_list: completeName = r'C:\Users\Admin\Desktop\Projects\agent messer\sent_mail\\'+filename with open(completeName, 'wb') as f: f.write(image) att_list.append(completeName) return att_list def beautiful_body(self, dict): outup = '' for key in dict.keys(): outup += str(key) + ': ' + str(dict[key]).replace('#S','') + '\n' return outup def main_input_proccess(self): self.update_sources() self.get_mail() self.semantic_manager() if len(self.df_to_score) > 0: for ind in self.df_to_score.index.values: issue_dict = {'project': 'BI', 'summary': str(self.df_to_score['subj'].iloc[ind]), 'description': str(self.df_to_score['text'].iloc[ind]), 'issuetype': {'name': 'Задача'}} current_issue = str(self.create_jira_issue(issue_dict, self.df_to_score['cust'].iloc[ind])) self.add_task_to_db(self.df_to_score['cust'].iloc[ind], self.search_jira_issue(current_issue)) self.send_mail_to_user(self.df_to_score['cust'].iloc[ind], 'По вашему обращению зарегистрирована задача: ' + current_issue + '\n\n', self.df_to_score['subj'].iloc[ind]) def main_output_proccess(self): self.get_jira_updates()