class Jira(object): def __init__(self, url, user, password): self.jira = JIRA(url, auth=(user, password)) def book_jira(self, bookings): for entry in bookings: if not ticket_pattern_jira.match(entry['issue_id']): continue rm_entry = entry.copy() rm_entry['hours'] = rm_entry['time'] / 60. del rm_entry['time'] if 'description' in rm_entry: del rm_entry['description'] if 'activity' in rm_entry: del rm_entry['activity'] try: self.jira.add_worklog( issue=entry['issue_id'], timeSpent=entry['time'], started=datetime.strptime(entry['spent_on'], '%Y-%m-%d'), comment=entry['comments'], ) except JIRAError as je: print(u'{0}: {1} ({2})'.format( je.status_code, je.text, rm_entry['comments']))
def update_jira(username, password): """Update time logs in Jira Current implementation uses basic authentication, future version will have better auth. For simplicity the toggl log should have issue number as timelog description. *username* to use to connect to Jira *password* for Jira authentication """ url = CONFIG.get('jira')['url'] jira = JIRA( options={'server': url}, basic_auth=(username, password)) time_logs = toggl.Connect('jira') get_logs = time_logs.get_time_logs() for each_log in get_logs: issue_id = each_log.get('description') try: issue = jira.issue(issue_id) except JIRAError: logging.warning('Failed to find issue-id {0}'.format(issue_id)) continue # Compute time in hours and round to two decimal places time_in_hours = round(each_log['duration']/(60*60), 2) jira.add_worklog(issue, time_in_hours, comment='Updated using Jira API')
def execute(): record = helpers.load_record() current = record['current'] workLogList = helpers.work_log_list(record, current) if len(workLogList) != 0: jira = JIRA('https://contentwatch.atlassian.net', basic_auth=(passwordHide['username'], passwordHide['password'])) issue = jira.issue(current) msg.jira_item_being_logged(issue.fields.summary) print "----------------------------------" for item in workLogList: print 'Time: {time}'.format( time=helpers.time_worked(item['spent'])) print for item in record['projects'][current]['time']: if item['spent'] != '': if 'jira_recorded' in item: if item['jira_recorded'] == 'False': timeWorked = helpers.time_worked(item['spent']) if timeWorked != '0h 00m': # print helpers.jira_start_date_format2(item['start']) jira.add_worklog(current, timeSpent=timeWorked, timeSpentSeconds=None, adjustEstimate=None, newEstimate=None, reduceBy=None, comment=helpers.work_log_comment( item['spent_date'], timeWorked), started=None, user=None) item['jira_recorded'] = 'True' else: timeWorked = helpers.time_worked(item['spent']) if timeWorked != '0h 00m': # print helpers.jira_start_date_format2(item['start']) jira.add_worklog(current, timeSpent=timeWorked, timeSpentSeconds=None, adjustEstimate=None, newEstimate=None, reduceBy=None, comment=helpers.work_log_comment( item['spent_date'], timeWorked), started=None, user=None) item['jira_recorded'] = 'True' content = helpers.glue_updated_record(record) helpers.write_file(helpers.recordPath, content) print msg.process_completed() else: msg.nothing_to_log()
def _submit_registration(self): jira = JIRA( Configuration.get("server_url"), auth=( Configuration.get("username"), keyring.get_password(APPLICATION_NAME, Configuration.get("username")), ), ) jira.add_worklog(jira.issue(self.register_issue_number), self.register_time_spent)
class JiraConnector(object): address = "your jira adress here" def __init__(self): self.login = get_login_from_filename() self.connection = None def connect(self, password): """ create connection to JIRA server with credentials Args: password (str): password to JIRA account """ credentials = (self.login, password) self.connection = JIRA(server=self.address, basic_auth=credentials) def add_worklog(self, issue, hours, date, commentary): """ add worklog to specified issue with hours, date and commentary arguments Args: jira_connection (JIRA): JIRA interface object issue (str): issue code hours (str): how many hours to log date (datetime): date to log commentary (str): commentary to log """ assert self.connection, "not connected" self.connection.add_worklog(issue=issue, timeSpent=hours, comment=commentary, started=date) print("worklog sent.") def send(self, password, hours, issue, date, commentary=""): """ send worklog data to JIRA server - connect to server if not connected - add worklog Args: password (str): password to JIRA account hours (str): hours to log issue (str): issue code to log date (datetime): specified day to log commentary (str, optional): comment to log. Defaults to "". """ print("to send:", hours, issue, date, commentary) if not self.connection: self.connect(password=password) self.add_worklog(issue, hours, date, commentary)
class JiraClient: seconds_in_hour = 3600 def __init__(self, url, login, password) -> None: self.jira = JIRA(url, auth=(login, password)) def queryWorklog(self, work_day: date): issues = self.jira.search_issues( jql_str=f'worklogAuthor =currentUser() AND worklogDate = "{work_day}"', json_result=True, maxResults=10000 ) timespent = 0 start_of_date = datetime.combine(work_day, datetime.min.time()) end_of_date = datetime.today().replace(year=work_day.year, month=work_day.month, day=work_day.day, hour=23, minute=59, second=59) for row in issues['issues']: worklogs = self.jira.worklogs(row['key']) for wl in worklogs: logged_at = datetime.strptime(wl.started, "%Y-%m-%dT%H:%M:%S.%f%z").replace(tzinfo=None) if start_of_date <= logged_at < end_of_date: max_seconds_in_day = (end_of_date - logged_at).seconds timespent += min(wl.timeSpentSeconds, max_seconds_in_day) return timespent / self.seconds_in_hour def queryIssues(self, jql_str, fetch_size=10000): issues = self.jira.search_issues( jql_str=jql_str, json_result=True, maxResults=fetch_size, fields=None ) return issues['issues'] def add_worklog(self, issue, hours, work_date): self.jira.add_worklog( issue, timeSpentSeconds=hours * self.seconds_in_hour, started=work_date, comment="auto log" )
def log_work(jira: JIRA, events: list, task_id: str) -> None: dt_format = '%Y-%m-%dT%H:%M:%S%z' event_count = 0 total_count = len(events) work_logs = jira.worklogs(task_id) for event in events: try: original_start_dt_str = event['originalStartTime']['dateTime'] if 'originalStartTime' in event \ else event['start']['dateTime'] original_start_dt = datetime.datetime.strptime( original_start_dt_str, dt_format) start_dt = datetime.datetime.strptime(event['start']['dateTime'], dt_format) end_dt = datetime.datetime.strptime(event['end']['dateTime'], dt_format) started = get_dt_jira_format(original_start_dt) comment = event['summary'] duration = end_dt - start_dt for work_log in work_logs: if work_log.started == started and work_log.comment == comment \ and work_log.timeSpentSeconds == duration.total_seconds(): print( f'Event {comment} which started {started} will skip because item already exists' ) break else: jira.add_worklog(issue=task_id, timeSpentSeconds=duration.total_seconds(), started=original_start_dt, comment=comment) print( f'Event {comment} which started {started} was sent successfully' ) event_count = event_count + 1 except Exception as ex: print(f'Can not process event : {event}. Error = {ex}') print(f'{event_count} events from {total_count} were sent to jira')
def sync_ledger_jira(server, email, password, ledgerfile, ledgertag, assocfile, no_delete): ticket_mappings = read_ledger_jira_assoc(assocfile) all_entries = read_ledger(ledgerfile, [ledgertag]) jira_client = JIRA(basic_auth=(email, password), server=server) entries_to_add, entries_to_remove = determine_adds_and_deletes(jira_client, email, ledgertag, ticket_mappings, all_entries) if no_delete: entries_to_remove = [] if not entries_to_remove and not entries_to_add: print 'Nothing to do' if entries_to_remove: print 'I will delete the following entries: ' for e in entries_to_remove: print e if entries_to_add: print 'I will add the following entries: ' for e in entries_to_add: print e if raw_input('Press enter to continue <enter>') != '': return if entries_to_remove: print'Deleting...' for entry_to_remove in entries_to_remove: entry_to_remove.jira_log.delete() print 'Done' if entries_to_add: print'Adding...' for entry_to_add in entries_to_add: date = datetime.strptime(entry_to_add.date, '%Y-%m-%d') date = datetime(date.year, date.month, date.day, 17, tzinfo=UTC()) comment = entry_to_add.comment jira_client.add_worklog(entry_to_add.jira_id, None, int(entry_to_add.time_spent_seconds), None, None, None, comment, date) print 'Done'
def log_to_jira(data, jira_url, username, password): jira = JIRA(options={ "server": jira_url, "verify": False }, auth=(username, password)) skipped = {} for issue, wls in to_log.items(): if jira.issue(issue): for wl in wls: jira.add_worklog(issue, timeSpent=wl.get('timeSpent'), started=wl.get('dateStarted'), comment=wl.get('comment')) print(f'Updated {issue}: {wl.get("timeSpent")}') else: skipped[issue] = wls if skipped: print('WARNING: The following issues were skipped:') print_summary(skipped)
if __name__ == "__main__": args = get_args() jira = JIRA(args.jira_server, auth=(args.username, args.password)) weekend_diff = 0 print(f'Adding worklog for jira {args.jira_id}...') start_date_obj = parse(args.start_date) if args.end_date: end_date_obj = parse(args.end_date) days = 36500 else: end_date_obj = start_date_obj + timedelta(days=36500) days = int(args.days) for d in range(0, days): log_date = parse(args.start_date) + timedelta(days=d + weekend_diff) # add 1-2 days if it's a weekend if args.only_workday: log_date, weekend_diff = log_date_if_weekend( log_date, weekend_diff) if log_date > end_date_obj: break print(f'Adding {args.time_spent} for {log_date} to {args.jira_id}') jira.add_worklog(args.jira_id, timeSpent=args.time_spent, started=log_date) print('Done.')
class Strategy: def __init__(self, account_name, user_name, user_password): self._account = account_name self._user = user_name self._password = user_password self._server = 'https://{}.atlassian.net'.format(self._account) self._jira_connection = JIRA(server=self._server, basic_auth=(self._user, self._password)) self._makelog = makelog.Makelog('output', 'errorlog') def execute(self, key): if key == 1: self._doreporting() elif key == 2: self._domailing() elif key == 3: self._dogenerating() else: return False def _doreporting(self): data_peruser = {} data_percomponent = {} # getting all users users_all = self._jira_connection.search_users('%', maxResults=False, includeInactive=True) for user in users_all: data_peruser[user.name] = { 'time_total': 0, 'time_perissue': {}, 'actual_name': user.displayName, 'components': set() } # getting all components components_all = set() projects_all = self._jira_connection.projects() for project in projects_all: try: comps = self._jira_connection.project_components(project) components_all.update(comps) except: outstr = "Unexpected error with getting components from project: {}\n".format( project.key) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) for comp in components_all: try: component_data = self._jira_connection.component(comp.id) data_percomponent[component_data.id] = { 'name': component_data.name, 'projectkey': component_data.project, 'time_total': 0, 'time_perissue': {}, 'lead': '' if not hasattr(component_data, 'lead') else component_data.lead.name } if hasattr(component_data, 'lead'): data_peruser[component_data.lead.name]['components'].add( component_data.id) except: outstr = "Unexpected error with getting data of component id: {}\n".format( comp.id) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # counting hours logic issues_all = self._jira_connection.search_issues('', maxResults=False) for iss in issues_all: try: iss_works = self._jira_connection.worklogs(iss) for work in iss_works: # per user data_peruser[work.author. name]['time_total'] += work.timeSpentSeconds if iss.key not in data_peruser[ work.author.name]['time_perissue']: data_peruser[work.author.name]['time_perissue'][ iss.key] = 0 data_peruser[work.author.name]['time_perissue'][ iss.key] += work.timeSpentSeconds # per valid component (with lead) for comp in iss.fields.components: if data_percomponent[ comp.id]['lead'] == work.author.name: data_percomponent[ comp.id]['time_total'] += work.timeSpentSeconds if iss.key not in data_percomponent[ comp.id]['time_perissue']: data_percomponent[comp.id]['time_perissue'][ iss.key] = 0 data_percomponent[comp.id]['time_perissue'][ iss.key] += work.timeSpentSeconds except: outstr = "Unexpected error counting hours with issue: {}\n".format( iss.key) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # outputting data outstr = "" outstr += "\t\t\tReport on the spent hours:\n" outstr += "\n\t\tPer programmer:\n\n" for user_name, user_dat in data_peruser.iteritems(): outstr += "-> Name: {} ({})\n".format(user_dat['actual_name'], user_name) outstr += " Total time: {} hour(s)\n".format( str(user_dat['time_total'] / 3600)) outstr += " Time per issue:\n" for iss_key, time_val in user_dat['time_perissue'].iteritems(): outstr += "\t{} is: {} hour(s)\n".format( iss_key, str(time_val / 3600)) outstr += "\n" outstr += "\n\t\tPer component (with lead only):\n\n" for comp_id, comp_dat in data_percomponent.iteritems(): outstr += "-> Name: {} ({})\n".format(comp_dat['name'], comp_dat['projectkey']) outstr += " Lead: {}\n".format(comp_dat['lead']) outstr += " Total time: {} hour(s)\n".format( str(comp_dat['time_total'] / 3600)) outstr += " Time per issue:\n" for iss_key, time_val in comp_dat['time_perissue'].iteritems(): outstr += "\t{} is: {} hour(s)\n".format( iss_key, str(time_val / 3600)) outstr += "\n" outstr += "\n-----> END REPORT <-----\n\n" self._makelog.putto_console(outstr, iscln=True) self._makelog.putto_file(outstr) def _domailing(self): issues_tonotify = [] issues_all = self._jira_connection.search_issues('', maxResults=False) for iss in issues_all: try: iss_data = self._jira_connection.issue(iss) if (iss_data.fields.timeestimate is None) or (len( iss_data.fields.components) == 0): issues_tonotify.append({ 'name': iss_data.fields.assignee.name, 'dispname': iss_data.fields.assignee.displayName, 'email': iss_data.fields.assignee.emailAddress, 'isskey': iss.key }) except: outstr = "Unexpected error with getting issue: {}\n".format( iss.key) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) for data in issues_tonotify: try: url = "{}/rest/api/2/issue/{}/notify".format( self._server, data['isskey']) notify_data = { "subject": "You have some incomplete fields in issue {}".format( data['isskey']), "textBody": "Your got this notification because have one or couple incomplete fields in {} issue. Note, that 'estimates' \ and 'component' fields are mandatory. Please, check this fields and fill its in if need." .format(data['isskey']), "to": { "users": [{ "name": data['name'] }] }, } requests.post(url, auth=(self._user, self._password), json=notify_data) outstr = "Successfully sending notification to:\n-> {} {} about incomplete fields in {} issue\n".format( data['dispname'], data['email'], data['isskey']) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Unexpected error with sending notification to:\n-> {} {} about: {}\n".format( data['dispname'], data['email'], data['isskey']) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) if len(issues_tonotify) == 0: self._makelog.putto_console( "All tested issues were filed in correct") def _dogenerating(self): names_base = namebase.Namebase() maxlen_projname = 10 content_count = { 'project': 1, 'user': 1, 'component': 2, 'issue': 10, 'worklog': 20 } # making projects for i in xrange(content_count['project']): newname = names_base.getname_project() parts = newname.split()[::2] newkey = string.join( (parts[0][:(maxlen_projname - len(parts[1]))], parts[1]), '') try: self._jira_connection.create_project(newkey, name=newname) outstr = "Project {} was successfully created\n".format(newkey) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Some problem with project {} creation\n".format( newkey) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # making users for i in xrange(content_count['user']): newname = names_base.getname_user() try: self._jira_connection.add_user(newname, "{}@mail.net".format(newname),\ fullname="Name {}{}".format(string.upper(newname[:1]), newname[1:])) outstr = "User {} was successfully created\n".format(newname) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Some problem with user {} creation\n".format(newname) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # getting all valid project keys projects_keys = [] projects_all = self._jira_connection.projects() for project in projects_all: projects_keys.append(project.key) # getting all valid user names users_keys = [] users_all = self._jira_connection.search_users('%', maxResults=False, includeInactive=True) for user in users_all: users_keys.append(user.name) # making components for i in xrange(content_count['component']): newname = names_base.getname_component() try: self._jira_connection.create_component( newname, random.choice(projects_keys), leadUserName=random.choice(users_keys)) outstr = "Component {} was successfully created\n".format( newname) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Some problem with component {} creation\n".format( newname) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # making issues for i in xrange(content_count['issue']): newname = names_base.getname_issue() fields = { "project": { "key": random.choice(projects_keys) }, "summary": "Here should be some random text summary for issue {}".format( newname), "description": "Here should be some random text description for issue {}". format(newname), "issuetype": { "name": random.choice( ("Bug", "Improvement", "Task", "Epic", "New Feature")) }, "assignee": { "name": random.choice(users_keys) }, "timetracking": { "originalEstimate": "{}w {}d {}h".format(random.randint(1, 3), random.randint(1, 4), random.randint(1, 7)), "remainingEstimate": "{}d {}h".format(random.randint(1, 4), random.randint(1, 7)) } } try: self._jira_connection.create_issue(fields=fields) outstr = "Issue {} was successfully created\n".format(newname) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Some problem with issue {} creation\n".format( newname) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc()) # making worklogs issues_all = self._jira_connection.search_issues('', maxResults=False) for i in xrange(content_count['worklog']): iss = random.choice(issues_all) try: self._jira_connection.add_worklog(iss, timeSpent="{}h".format(random.randint(1, 3)), user=random.choice(users_keys),\ comment="Here should be some random text about work on this issue") outstr = "Worklog for issue {} was successfully created\n".format( iss.key) self._makelog.putto_console(outstr) self._makelog.putto_file(outstr) except: outstr = "Some problem with worklog creation for issue {}\n".format( iss.key) self._makelog.putto_console(outstr) self._makelog.putto_errorlog(outstr, traceback.format_exc())
class LogJammin: mode = 'date' current_date = None parse_only = False logs = [] tickets = [] jira = None time_zone = None now = None def __init__(self, filename, parse_only): self.parse_only = parse_only try: config = self.load_config() self.time_zone = timezone(config['time_zone']) self.now = self.time_zone.localize(datetime.now()) if not filename: if 'log_file' not in config or not config['log_file']: raise Exception('Log file not set') filename = config['log_file'] filename = realpath(expanduser(filename)) except Exception as e: self.exit_with_error(e) if not self.parse_only: print('Connecting to JIRA...', end='', flush=True) try: self.jira = JIRA(server=config['host'], basic_auth=(config['user'], config['password'])) except Exception as e: self.exit_with_error(e) print('\033[92mdone\033[0m') print('Loading logs...', end='', flush=True) try: self.load_logs(filename) except Exception as e: self.exit_with_error(e) print('\033[92mdone\033[0m') if not len(self.logs): self.exit_with_error('No logs found') self.print_summary() if not self.parse_only: while True: run = input('Upload logs to JIRA? (y/n): ').lower().strip() if run == 'n': self.exit_with_success() elif run == 'y': break try: for (i, log) in enumerate(self.logs): print('Saving log {}/{}: ({})...'.format( i + 1, len(self.logs), self.format_log(log)), end='', flush=True) self.upload_log(log) print('\033[92mdone\033[0m') except Exception as e: self.exit_with_error(e) self.exit_with_success() def print_summary(self): logs_by_date = OrderedDict() total_minutes = 0 print('\033[94m{}\033[0m'.format(80 * '=')) print('\033[93mSummary:\033[0m') for log in self.logs: date = log['date'].strftime('%Y-%m-%d') if date not in logs_by_date: logs_by_date[date] = {'logs': [], 'total_time_minutes': 0} logs_by_date[date]['logs'].append(log) logs_by_date[date][ 'total_time_minutes'] += 60 * log['time']['hours'] logs_by_date[date]['total_time_minutes'] += log['time']['minutes'] for date, summary in logs_by_date.items(): print('\n\033[93m{}\033[0m'.format(date)) hours = math.floor(summary['total_time_minutes'] / 60) minutes = math.floor(summary['total_time_minutes'] % 60) total_minutes += summary['total_time_minutes'] for log in summary['logs']: time = self.format_time(log['time']['hours'], log['time']['minutes']) description = '({})'.format( log['description']) if log['description'] else '' print(' {}: {} {}'.format(log['ticket'], time, description)) print('\033[93mTotal: {} logs, {}\033[0m'.format( len(summary['logs']), self.format_time(hours, minutes))) summary_hours = math.floor(total_minutes / 60) summary_minutes = math.floor(total_minutes % 60) print('\n\033[93mSum Total: {} days, {} logs, {}\033[0m'.format( len(logs_by_date), len(self.logs), self.format_time(summary_hours, summary_minutes))) print('\033[94m{}\033[0m'.format(80 * '=')) def exit_with_success(self): print('\033[92mDone\033[0m') exit() def exit_with_error(self, e): print('\n\033[91m{}\033[0m'.format(str(e))) exit(1) def format_log(self, log): return 'date={}, ticket={}, time={}, description={}'.format( log['date'].strftime('%Y-%m-%d'), log['ticket'], self.format_time(log['time']['hours'], log['time']['minutes']), log['description']) def format_time(self, hours, minutes): time_str = '' if hours: time_str += '{}h '.format(hours) if minutes: time_str += '{}m'.format(minutes) return time_str.strip() def load_config(self): try: required_keys = ['user', 'password', 'host', 'time_zone'] with open(expanduser('~/.logjammin')) as f: config = json.load(f) for key in required_keys: if key not in config: raise Exception('missing key \'{}\''.format(key)) return config except Exception as e: raise Exception( 'Error parsing ~/.logjammin: {}'.format(e)) from None def load_logs(self, filename): line_no = 0 loading_pct = 0 with open(filename, 'r') as fp: lines = fp.read().splitlines() for line in lines: line_no += 1 stripped_line = line.strip() if not len(stripped_line): continue if stripped_line.startswith('//') or stripped_line.startswith('#'): continue try: self.parse_line(stripped_line) except Exception as e: raise Exception('Error on line {}: {}'.format( line_no, str(e))) from None prev_loading_pct = loading_pct loading_pct = math.floor(line_no / len(lines) * 100) print('{}{}%'.format( '\b' * (len(str(prev_loading_pct)) + 1 if prev_loading_pct else 0), loading_pct), end='', flush=True) if len(lines): print('\b' * 4, end='', flush=True) # 100% self.logs.sort(key=lambda k: (k['date'], k['ticket'].split('-')[0], int(k['ticket'].split('-')[1]))) def upload_log(self, log): time_spent = '{}h {}m'.format(log['time']['hours'], log['time']['minutes']) kwargs = {'comment': log['description']} if log['description'] else {} self.jira.add_worklog(issue=log['ticket'], timeSpent=time_spent, started=log['date'], **kwargs) def parse_line(self, line): normalized_line = re.sub(r'\s+', ' ', line.strip()) if self.mode == 'date': try: self.current_date = self.parse_date_line(normalized_line) self.mode = 'time_log' except Exception as e: raise Exception('String \'{}\' is invalid: {}'.format( line, str(e))) from None elif self.mode == 'time_log': try: ticket, time, description = self.parse_time_log_line( normalized_line) self.add_log(ticket, time, description) self.mode = 'date_or_time_log' except Exception as e: raise Exception('String \'{}\' is invalid: {}'.format( line, e)) from None elif self.mode == 'date_or_time_log': try: self.mode = 'date' return self.parse_line(line) except Exception as e: try: self.mode = 'time_log' return self.parse_line(line) except Exception as e: raise Exception('String \'{}\' is invalid: {}'.format( line, str(e))) from None else: raise Exception('Invalid mode \'{}\''.format(self.mode)) def parse_date_line(self, line): date_match = re.match( r'^(?P<year>\d{4})-?(?P<month>\d{2})-?(?P<day>\d{2})$', line) if not date_match: raise Exception('Pattern not matched') date = self.time_zone.localize( datetime(int(date_match.group('year')), int(date_match.group('month')), int(date_match.group('day')))) if date > self.now: raise Exception('Date is in the future') return date def parse_time_log_line(self, line): parts = line.split(',', 2) ticket_str = parts[0].strip() if len(parts) else '' time_str = parts[1].strip() if len(parts) > 1 else '' description = parts[2].strip() if len(parts) > 2 else '' ticket_match_re = r'^[A-Z][A-Z0-9]+-\d+$' ticket_match = re.match(ticket_match_re, ticket_str, re.IGNORECASE) if not ticket_match: raise Exception('Ticket pattern not matched') ticket = ticket_match.group(0).upper() hours = 0 minutes = 0 dec_hours_match_re = '^(\d+\.\d+|\.\d+|\d+)\s*H?$' dec_hours_match = re.match(dec_hours_match_re, time_str, re.IGNORECASE) if dec_hours_match: dec_hours = float(dec_hours_match.group(1)) hours = math.floor(dec_hours) minutes = math.floor(60 * (dec_hours % 1)) else: hours_mins_match_re = r'((?P<hours>\d+)\s*H)?\s*((?P<minutes>\d+)\s*M)?' hours_mins_match = re.match(hours_mins_match_re, time_str, re.IGNORECASE) if hours_mins_match: hours = int(hours_mins_match.group('hours') or 0) minutes = int(hours_mins_match.group('minutes') or 0) if not hours and not minutes: raise Exception('Time pattern not matched') if not self.parse_only: self.assert_ticket_exists(ticket) time = (hours, minutes) return (ticket, time, description) def assert_ticket_exists(self, ticket): if ticket in self.tickets: return try: self.jira.issue(ticket, fields='key') self.tickets.append(ticket) except Exception as e: raise Exception( 'Failed to get ticket info for {}'.format(ticket)) from None def add_log(self, ticket, time, description): self.logs.append({ 'date': self.current_date, 'ticket': ticket, 'description': description, 'time': { 'hours': time[0], 'minutes': time[1] } })
parser.add_argument('-d', '--date', type=str, metavar='Date', required=True, help="Format: 2018-06-28 18:00") parser.add_argument('-n', '--description', type=str, metavar='Description', required=True, help="Fill in what you have done") parser.add_argument('-w', '--worked', type=str, metavar='Time-Worked', required=True, help="Time worked, format: 2h, 1d") args = parser.parse_args() datetime = datetime.strptime(args.date, '%Y-%m-%d %H:%M') datetime = tz.localize(datetime) #exit(0) # https://jira.readthedocs.io/en/master/api.html#jira.JIRA.add_worklog jira.add_worklog(args.jira_item, timeSpent=args.worked, started=datetime, comment=args.description)
class JiraLogger: def __init__(self): warnings.filterwarnings( 'ignore' ) # SNIMissingWarning and InsecurePlatformWarning is printed everytime a query is called. This is just to suppress the warning for a while. try: self.params = self.__get_params_from_config() self.jira = JIRA(server=self.params['server'], basic_auth=(self.params['username'], self.params['password'])) except JIRAError: raise RuntimeError( "Something went wrong in connecting to JIRA. Please be sure that your server, username and password are filled in correctly." ) else: # self.__log_work_for_sprint() self.populate_dict() def populate_dict(self): print 'Fetching data from JIRA server. This will take a while...' issues = self.__fetch_all_issues_for_project() issues = self.__filter_resolved_and_closed_issues(issues) self.__fetch_all_worklogs_for_issues(issues) self.__filter_worklogs_not_for_this_sprint(issues) self.__filter_worklogs_not_from_user(issues) # pretty = prettify.Prettify() # print pretty(self.__get_total_timespent_per_day_of_sprint(issues)) def __fetch_all_issues_for_project(self): return self.jira.search_issues('project={}'.format( self.params['project']), maxResults=False) # TODO: move formatting to another function def __filter_resolved_and_closed_issues(self, issues): filtered_issues = {} for issue in issues: if not (str(issue.fields.status) == 'Resolved' or str(issue.fields.status) == 'Closed'): filtered_issues[issue.id] = { 'key': issue.key, 'summary': issue.fields.summary, 'assignee': issue.fields.assignee, 'reporter': issue.fields.reporter, 'status': issue.fields.status.name, 'issuetype': issue.fields.issuetype.name, 'subtasks': [subtask.id for subtask in issue.fields.subtasks], 'worklogs': [worklog.id for worklog in self.jira.worklogs(issue.id)] } return filtered_issues def __fetch_all_worklogs_for_issues(self, issues): for issue_id, issue_details in issues.items(): worklogs_list = {} for worklog_id in issue_details['worklogs']: worklogs_list.update( self.__fetch_worklog_details(issue_id, worklog_id)) issue_details['worklogs'] = worklogs_list return issues # TODO: move formatting to another function def __fetch_worklog_details(self, issue_id, worklog_id): worklog = self.jira.worklog(issue_id, worklog_id) return { worklog.id: { 'author': worklog.author, 'date': datetime.strptime(worklog.started[:10], '%Y-%m-%d').strftime('%Y-%m-%d'), 'timespent': worklog.timeSpent, 'comment': worklog.comment } } def __filter_worklogs_not_for_this_sprint(self, issues): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items( ): if worklog_details['date'] not in dates: del issue_details['worklogs'][worklog_id] def __filter_worklogs_not_from_user(self, issues): for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items( ): if not worklog_details['author'].name == self.username: del issue_details['worklogs'][worklog_id] def __get_total_timespent_per_day_of_sprint(self, issues): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) worklogs = {} for date in dates: worklogs[date] = [] for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items( ): worklogs[worklog_details['date']].append( worklog_details['timespent']) return { date: helper.to_time(sum(map(helper.parse_time, timespent))) for date, timespent in worklogs.items() } # REMOVE: FRONTEND def __get_start_and_end_date_for_sprint(self): sprint_dates = { '1602.1': ['2016-01-13', '2016-01-26'], '1602.2': ['2016-01-27', '2016-02-16'], '1603.1': ['2016-02-17', '2016-03-01'], '1603.2': ['2016-03-02', '2016-03-15'], '1604.1': ['2016-03-16', '2016-04-05'], '1604.2': ['2016-04-05', '2016-04-19'], '1605.1': ['2016-04-20', '2016-05-03'] }.get(self.params['sprint_id'], None) if sprint_dates is None: raise RuntimeError('{} is not a proper sprint id.'.format( self.params['sprint_id'])) return sprint_dates def __generate_date_list(self, start, end): start = datetime.strptime(start, '%Y-%m-%d') end = datetime.strptime(end, '%Y-%m-%d') dates = [] for day in range(0, (end - start).days + 1): date = start + timedelta(days=day) if date.weekday() not in [5, 6] and date.strftime( '%Y-%m-%d') not in helper.get_holidays_list().keys(): dates.append(date.strftime('%Y-%m-%d')) return dates def __get_params_from_config(self): with open('sample.json') as data_file: try: data = json.load(data_file) except ValueError: raise RuntimeError( "There was something wrong in you config.json. Please double check your input." ) return data def __log_work_for_sprint(self): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) # TODO: check if already logged. Maybe change logging per day instead. # TODO: check if exceeds time. Print warning before actually logging. print 'Logging work.' # self.__log_holidays(sprint_dates) # self.__log_leaves() self.__log_daily_work(dates) # self.__log_meetings() # self.__log_sprint_meetings(sprint_dates) # self.__log_trainings() # self.__log_reviews() # self.__log_other_tasks() def __log_holidays(self, sprint_dates): holidays = helper.get_holidays_list() print 'Logging holidays...' for holiday in holidays: if sprint_dates[0] <= holiday <= sprint_dates[1]: worklog = self.jira.add_worklog( self.params['holidays_id'], '8h', started=parser.parse(holiday + 'T08:00:00-00:00'), comment=holidays[holiday]) if not isinstance(worklog, int): raise RuntimeError( 'There was a problem logging your holidays.') def __log_leaves(self): # TODO: Support for not whole day leaves print 'Logging your leaves...' for leave in self.params['leaves']: worklog = self.jira.add_worklog( leave['id'], leave['timeSpent'], started=parser.parse(leave['started'] + 'T08:00:00-00:00'), comment=leave['comment']) print worklog if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your leaves.') def __log_daily_work(self, dates): print 'Logging your daily tasks...' for task in self.params['daily_tasks']: for date in dates: worklog = self.jira.add_worklog( task['id'], task['timeSpent'], started=parser.parse(date + 'T08:00:00-00:00'), comment=task['comment']) if not isinstance(worklog, int): raise RuntimeError( 'There was a problem logging your daily work.') def __log_meetings(self): print 'Logging your meetings...' for meeting in self.params['meetings']: worklog = self.jira.add_worklog( meeting['id'], meeting['timeSpent'], started=parser.parse(meeting['started'] + 'T08:00:00-00:00'), comment=meeting['comment']) if not isinstance(worklog, int): raise RuntimeError( 'There was a problem logging your meetings.') def __log_sprint_meetings(self, sprint_dates): print 'Logging your sprint meetings...' for sprint_meeting in self.params['sprint_meetings']: worklog = worklog = self.jira.add_worklog( sprint_meeting['id'], sprint_meeting['timeSpent'], started=parser.parse(sprint_meeting['started'] + 'T08:00:00-00:00'), comment=sprint_meeting['comment']) if not isinstance(worklog, int): raise RuntimeError( 'There was a problem logging your sprint meetings.') def __log_trainings(self): print 'Logging your trainings...' for training in self.params['trainings']: worklog = self.jira.add_worklog( training['id'], training['timeSpent'], started=parser.parse(training['started'] + 'T08:00:00-00:00'), comment=training['comment']) if not isinstance(worklog, int): raise RuntimeError( 'There was a problem logging your trainings.') def __log_reviews(self): # TODO: Find a way to automate this print 'Logging your reviews...' for review in self.params['reviews']: worklog = self.jira.add_worklog( self.params('reviews_id'), '{}h'.format(.5 * len(reviews[review])), started=parser.parse(review + 'T08:00:00-00:00'), comment='\n'.join(reviews[review])) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your reviews.') def __log_other_tasks(self): # TODO: Make this a filler task function. print "Not yet supported"
elif (len(sys.argv) == 4 and str(sys.argv[1]) == "transition"): issue = sys.argv[2] transition = sys.argv[3] if (transition == "Start progress"): timelog = open(".jira_progressstarted.txt", "w+") timelog.write(str(datetime.now().timestamp())) timelog.close() if (transition == "Resolved" or transition == "Stop progress"): timelog = open(".jira_progressstarted.txt", "r") timeStarted = datetime.fromtimestamp(float(timelog.readline())) timeNow = datetime.now() diffMinutes = (timeNow - timeStarted).seconds / 60 if diffMinutes >= 1: jira.add_worklog(issue, timeSpent=str(diffMinutes) + 'm', comment="Auto-logged by JIRA Issue Manager") os.remove(".jira_progressstarted.txt") if (transition == "Resolved"): jira.transition_issue(issue, transition, assignee=sys.argv[4]) else: jira.transition_issue(issue, transition) exit(0) # HELPER FUNCTIONS def getAsBase64(url): return base64.b64encode(requests.get(url).content).decode('utf-8')
def post_outlook_to_jira(): OUTLOOK_FORMAT = '%d.%m.%Y %H:%M' outlook = Dispatch("Outlook.Application") ns = outlook.GetNamespace("MAPI") config_filename = 'config.conf' path_to_config = os.path.join(Path(os.path.dirname(__file__)).parent, config_filename) # check for existence of config file if not os.path.exists(path_to_config): print('Config.conf does not exist. Generating default config.') generate_default_config(path_to_config=path_to_config) # open config config = configparser.RawConfigParser(allow_no_value=True) config.read(path_to_config) # check for a folderPath in config folder_path = config.get('Outlook', 'folder_path') if folder_path != '': print(f'Last time you used the following calendar: {folder_path}.') print('Press enter to use it again.') print('Type default to use the Outlook default calender') print('Enter a new if you want to use a new calendar.') user_input = input() if user_input != '': folder_path = user_input else: print('Press enter to use the Outlook default calender') print('Enter a new if you want to use a new calendar.') folder_path = input() if folder_path == '': folder_path = 'default' # clear folder_path from config write_config(config, path_to_config, section='Outlook', key='folder_path', value='') else: write_config(config, path_to_config, section='Outlook', key='folder_path', value=folder_path) # read category name to mark all processed items in outlook processed_category = config.get('Outlook', 'processed_category') if processed_category == '': processed_category_default = 'jira_logged' print('') print('Please input a name for the category which should be used to mark processed appointments.') user_input = input(f'Leave blank to use the programs default: "{processed_category_default}"') if user_input == '': processed_category = processed_category_default else: processed_category = user_input # write new category name to config write_config(config, path_to_config, section='Outlook', key='processed_category', value=processed_category) # check if the category is already present in outlook category_found = False for category in ns.Categories: if str(category) == processed_category: category_found = True if category_found == False: print('') try: ns.Categories.Add(processed_category) print(f'Info: Added category to Outlook: {processed_category}') except: print('Could not add category to Outlook.') sys.exit(1) #get all appointments from outlook print('') print('Please enter the date (format: YYYY-MM-DD) were you want to start processing items.') begin = input('Press ENTER to use default (today): ') appointments = get_outlook_appointments(config, path_to_config=path_to_config, ns=ns, begin=begin) # ask for JIRA instance jira_url = config.get('Jira', 'url') if jira_url == '': while jira_url == '': print('') jira_url = input('Please specify the JIRA instance (URL): ') else: jira_input = input(f'Last time you connected to {jira_url}. Press ENTER to use it again or enter a new JIRA instance: ') if jira_input != '': jira_url = jira_input # write new JIRA URL to config write_config(config, path_to_config, section='Jira', key='url', value=jira_url) # ask for username jira_user = config.get('Jira', 'username') if jira_user == '': while jira_user == '': print('') jira_user = input('Please specify your username for JIRA: ') else: jira_input = input(f'Last time you connected to JIRA using {jira_user}. Press ENTER to use it again or enter a new JIRA username: '******'': jira_user = jira_input # write new JIRA username to config write_config(config, path_to_config, section='Jira', key='username', value=jira_user) # init some variables jira_password = '' use_token = '' # if JIRA cloud -> API Token needed if '.atlassian.net' in jira_url: print('') print('=====================================================') print('You are trying to connect to a JIRA Cloud instance.') print('Please make sure to generate an API Token at:') print('') print('https://id.atlassian.com/manage/api-tokens') print('') print('That API Token is then used instead of your Password.') print('=====================================================') print('') api_token = config.get('Jira', 'api_token') if api_token != '': use_token = input(f'You already saved an API Token. Do you want to use it again? y = yes, n = no: ') if use_token.upper() == 'Y': jira_password = api_token # ask for password/ token and try to authenticate if jira_password == '': jira_password = getpass() # ask if api_token should be stored if '.atlassian.net' in jira_url and use_token.upper() != 'Y': print('') save_token = input('Do you want to save the API Token for the next session? y = yes, n = no: ') if save_token.upper() == 'Y': write_config(config, path_to_config, section='Jira', key='api_token', value=jira_password) try: auth_jira = JIRA(jira_url, basic_auth=(jira_user, jira_password)) except: print(f'Could not authenticate with the given credentials.') sys.exit(1) print('') print('Processing Outlook Appointments...') for appointmentItem in appointments: # check if it was already processed before if processed_category in appointmentItem.Categories: print(f'[Info] Appointment "{appointmentItem.Subject}" is already logged in Jira') continue # check if the subject contains valid JIRA Issue ID, if there are multiple matches, pick the first. REGEX: [A-Z0-9]*-[0-9]* m = re.search('[A-Z0-9]*-[0-9]*', appointmentItem.Subject.upper()) try: ticket = m.group(0) except: # no valid Ticket ID String found print(f'[Info] Appointment "{appointmentItem.Subject}" does not contain a valid jira ticket id.') continue # check if there is a jira issue for the extracted ticket id try: issue = auth_jira.issue(ticket) except: print(f'[Info] Appointment "{appointmentItem.Subject}" could not be logged. There is no issue with the ID {ticket}') continue print(f'[Info] Processing outlook item "{appointmentItem.Subject}":') # log in jira jira.add_worklog("issue number", timeSpent="2h", comment="comment", started="") auth_jira.add_worklog(ticket,timeSpent=appointmentItem.Duration,comment=appointmentItem.Subject, started=appointmentItem.Start) print(f' Logged {appointmentItem.Duration} minutes on {ticket}.') # add processed category to outlook item and save it in outlook appointmentItem.Categories = appointmentItem.Categories + ', ' + processed_category appointmentItem.Save() print(f' Added the category {processed_category} to the Outlook item.')
class JiraHelper: def __init__(self, url, user, passwd, simulation): self.url = url self.simulation = simulation self.user_name = user if url: self.jira_api = JIRA(url, basic_auth=(user, passwd)) if simulation: print(colored("Jira is in simulation mode", Colors.IMPORTANT.value)) @staticmethod def round_to_minutes(seconds): return round(seconds / 60) * 60 @classmethod def dictFromTogglEntry(cls, togglEntry): # by default Jira truncates time to full minutes (cuts seconds portion of the time) # which creates big difference over a longer period of time rounded_to_minutes = cls.round_to_minutes(togglEntry.seconds) return { "issueId": togglEntry.taskId, "started": togglEntry.start, "seconds": rounded_to_minutes, "comment": "{} [toggl#{}]".format(togglEntry.description, togglEntry.id), } def get(self, issue_key): try: for worklog in self.jira_api.worklogs(issue_key): if worklog.author.name == self.user_name: yield JiraTimeEntry.fromWorklog(worklog, issue_key) except Exception as exc: raise Exception( "Error downloading time entries for {}: {}".format(issue_key, str(exc)) ) def put(self, issueId, started: datetime, seconds, comment): if isinstance(started, str): started = dateutil.parser.parse(started) if int(seconds) < 60: print( colored( "\t\tCan't add entries under 1 min: {}, {}, {}, {}".format( issueId, str(started), seconds, comment ), Colors.ERROR.value, ) ) return if self.simulation: print( "\t\tSimulate create of: {}, {}, {}, {}".format( issueId, str(started), seconds, comment ) ) else: # add_worklog "started" is expected as datetime self.jira_api.add_worklog( issueId, timeSpentSeconds=seconds, started=started, comment=comment ) def update(self, id, issueId, started, seconds, comment): # have to get the exact dt format, otherwise will get an Http-500 if isinstance(started, str): started = dateutil.parser.parse(started) started = started.strftime("%Y-%m-%dT%H:%M:%S.000%z") if int(seconds) < 60: print( colored( "\t\tCan't update entries to under 1 min, deleting instead: {}, {}, {}, {}".format( issueId, str(started), seconds, comment ), Colors.UPDATE.value, ) ) self.delete(id, issueId) return if self.simulation: print( "\t\tSimulate update of: {}, {}, {}, {} (#{})".format( issueId, started, seconds, comment, id ) ) else: worklog = self.jira_api.worklog(issueId, id) # update "started" is expected as str print("\t\tUpdate: {}s on {} with {}".format(seconds, started, comment)) worklog.update(timeSpentSeconds=seconds, started=started, comment=comment) def delete(self, id, issueId): if self.simulation: print("\t\tSimulate delete of: {}".format(id)) else: worklog = self.jira_api.worklog(issueId, id) worklog.delete() print(colored("\t\tDeleted entry for: {}".format(issueId), Colors.UPDATE.value))
class JiraToolsAPI: def __init__(self, jira_server_link, username=None, password=None): """Initalizes the Jira API connector. If a username or password is not provided you will be prompted for it. args: jira_server_link (str): Link to the Jira server to touch API kwargs: username (str): Overwrites jira username prompt password (str): Overwrites jira password prompt return: None """ self.jira_server_link = jira_server_link self.jira_options = {"server": self.jira_server_link} if username == None: username = input("Username: "******"Authenticated successfully with Jira with {self.username}") def create(self, data): """Create a single Jira ticket. args: data (dict): Fields required or needed to create the ticket. return (str): Ticket number / 'False' if fails """ try: jira_ticket = self._JIRA.create_issue(fields=data) logging.info( f"Successfully created Jira issue '{jira_ticket.key}'") return jira_ticket.key except Exception as error: logging.debug( f"Failed to create Jira issue '{jira_ticket.key}'\n\n{error}\n\n" ) return False def link(self, issue_from, issue_to, issue_link_name=None): """Link two issues together. Defaults to 'Relates' unless issue_link_name is specified. args: issue_from (str): Issue that will be linked from. issue_to (str): Issue that will be linked to. kwargs: issue_link_name (str): issue link name that should be applied. return (bool): Will return 'True' if it completed successfully. """ try: self._JIRA.create_issue_link(issue_link_name, issue_from, issue_to) logging.info( f"Successfully created a '{issue_link_name}' link between '{issue_from}' and '{issue_to}'." ) return True except Exception as error: logging.debug( f"Failed to create a link between '{issue_from}' and '{issue_to}'\n\n{error}\n\n" ) return False def label(self, issue, labels): """Apply labels to a given issue. args: issue (str): Issue that labels will be applied to. labels (list): list of labels that should be applied to the issue. Return (bool): Will return 'True' if it completed successfully. """ if type(labels) == list: try: issue_instance = self._JIRA.issue(issue) issue_instance.update( fields={"labels": issue_instance.fields.labels + labels}) logging.info( f"Successfully added labels '{labels}' to '{issue}'") return True except Exception as error: logging.debug( f"Failed to add labels '{labels}' to '{issue}'\n\n{error}\n\n" ) return False else: raise ScriptError('A list must be passed to the labels argument') def comment(self, issue, comment): """Apply a comment to a given issue. args: issue (str): Issue that comment will be applied to. comment (str): comment that should be applied to the issue. return (bool): Will return 'True' if it completed successfully. """ try: self._JIRA.add_comment(issue, comment) logging.info( f"Successfully added comment '{comment}' to '{issue}'") return True except Exception as error: logging.debug( f"Failed to add comment '{comment}' to '{issue}'\n\n{error}\n\n" ) return False def log_work(self, issue, time_spent, comment=None): """Log work to a given issue. args: issue (str): Issue to log work. time_spent (str): Time that should be logged to the issue. kwargs: comment (str): Description of what this time represents. return (bool): Will return 'True' if it completed successfully. """ try: if comment != None and type(comment) == str: self._JIRA.add_worklog(issue, time_spent, comment=comment) else: self._JIRA.add_worklog(issue, time_spent) logging.info(f"Successfully logged time to '{issue}'") return True except Exception as error: logging.info( f"Failed to log work to '{issue}' See debug logs for more.") logging.debug(f"\n{error}\n") return False def add_attachment(self, issue, attachment): """Attach file to Jira issue. args: issue (str): Issue name attachment (str): Location of file that should be attached. Return (bool): Will return 'True' if completed successfully """ assert isinstance(issue, str) assert isinstance(attachment, str) try: self._JIRA.add_attachment(issue=issue, attachment=attachment) logging.info(f'Successfully attached document to "{issue}"') return True except Exception as error: logging.debug( f"Failed to attach document to '{issue}'\n\n{error}\n\n") return False def update_status(self, id, end_status, transfer_statuses=[], timeout_attempts=10): """Change issue to desired status. Due to the workflow features of Jira it might not be possible to transition directly to the wanted status, intermediary statuses might be required and this funcation allows for that using 'transfer_statuses'. args: id (str): Issue id for status update end_status (str): Name of status to update ticket to. kwargs: transfer_statuses (list): Ordered list of intermediary statuses timeout_attempts (num): Number of times before while loop times out. return (bool): Will return 'True' if completed successfully """ while timeout_attempts != 0: transitions = self._JIRA.transitions(id) for transition in transitions: if transition['name'] == end_status: jira_ticket = self._JIRA.transition_issue( id, transition['id']) logging.info( f"Updated status of '{issue}' to '{end_status}'") return True elif transition['name'] in transfer_statuses: jira_ticket = self._JIRA.transition_issue( id, transition['id']) timeout_attempts -= 1 logging.debug( f"Failed to update status of '{id}' to end_status ({end_status})") return False
class JiraLogger: def __init__(self): warnings.filterwarnings('ignore') # SNIMissingWarning and InsecurePlatformWarning is printed everytime a query is called. This is just to suppress the warning for a while. try: self.params = self.__get_params_from_config() self.jira = JIRA(server=self.params['server'], basic_auth=(self.params['username'], self.params['password'])); except JIRAError: raise RuntimeError("Something went wrong in connecting to JIRA. Please be sure that your server, username and password are filled in correctly.") else: # self.__log_work_for_sprint() self.populate_dict() def populate_dict(self): print 'Fetching data from JIRA server. This will take a while...' issues = self.__fetch_all_issues_for_project() issues = self.__filter_resolved_and_closed_issues(issues) self.__fetch_all_worklogs_for_issues(issues) self.__filter_worklogs_not_for_this_sprint(issues) self.__filter_worklogs_not_from_user(issues) # pretty = prettify.Prettify() # print pretty(self.__get_total_timespent_per_day_of_sprint(issues)) def __fetch_all_issues_for_project(self): return self.jira.search_issues('project={}'.format(self.params['project']), maxResults=False) # TODO: move formatting to another function def __filter_resolved_and_closed_issues(self, issues): filtered_issues = {} for issue in issues: if not (str(issue.fields.status) == 'Resolved' or str(issue.fields.status) == 'Closed'): filtered_issues[issue.id] = { 'key': issue.key, 'summary': issue.fields.summary, 'assignee': issue.fields.assignee, 'reporter': issue.fields.reporter, 'status': issue.fields.status.name, 'issuetype': issue.fields.issuetype.name, 'subtasks': [subtask.id for subtask in issue.fields.subtasks], 'worklogs': [worklog.id for worklog in self.jira.worklogs(issue.id)] } return filtered_issues def __fetch_all_worklogs_for_issues(self, issues): for issue_id, issue_details in issues.items(): worklogs_list = {} for worklog_id in issue_details['worklogs']: worklogs_list.update(self.__fetch_worklog_details(issue_id, worklog_id)) issue_details['worklogs'] = worklogs_list return issues # TODO: move formatting to another function def __fetch_worklog_details(self, issue_id, worklog_id): worklog = self.jira.worklog(issue_id, worklog_id) return { worklog.id: { 'author': worklog.author, 'date': datetime.strptime(worklog.started[:10], '%Y-%m-%d').strftime('%Y-%m-%d'), 'timespent': worklog.timeSpent, 'comment': worklog.comment } } def __filter_worklogs_not_for_this_sprint(self, issues): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items(): if worklog_details['date'] not in dates: del issue_details['worklogs'][worklog_id] def __filter_worklogs_not_from_user(self, issues): for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items(): if not worklog_details['author'].name == self.username: del issue_details['worklogs'][worklog_id] def __get_total_timespent_per_day_of_sprint(self, issues): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) worklogs = {} for date in dates: worklogs[date] = [] for issue_id, issue_details in issues.items(): for worklog_id, worklog_details in issue_details['worklogs'].items(): worklogs[worklog_details['date']].append(worklog_details['timespent']) return {date: helper.to_time(sum(map(helper.parse_time, timespent))) for date, timespent in worklogs.items()} # REMOVE: FRONTEND def __get_start_and_end_date_for_sprint(self): sprint_dates = { '1602.1': ['2016-01-13', '2016-01-26'], '1602.2': ['2016-01-27', '2016-02-16'], '1603.1': ['2016-02-17', '2016-03-01'], '1603.2': ['2016-03-02', '2016-03-15'], '1604.1': ['2016-03-16', '2016-04-05'], '1604.2': ['2016-04-05', '2016-04-19'], '1605.1': ['2016-04-20', '2016-05-03'] }.get(self.params['sprint_id'], None) if sprint_dates is None: raise RuntimeError('{} is not a proper sprint id.'.format(self.params['sprint_id'])) return sprint_dates def __generate_date_list(self, start, end): start = datetime.strptime(start, '%Y-%m-%d') end = datetime.strptime(end, '%Y-%m-%d') dates = [] for day in range(0, (end-start).days + 1): date = start + timedelta(days=day) if date.weekday() not in [5, 6] and date.strftime('%Y-%m-%d') not in helper.get_holidays_list().keys(): dates.append(date.strftime('%Y-%m-%d')) return dates def __get_params_from_config(self): with open('sample.json') as data_file: try: data = json.load(data_file) except ValueError: raise RuntimeError("There was something wrong in you config.json. Please double check your input.") return data def __log_work_for_sprint(self): sprint_dates = self.__get_start_and_end_date_for_sprint() dates = self.__generate_date_list(sprint_dates[0], sprint_dates[1]) # TODO: check if already logged. Maybe change logging per day instead. # TODO: check if exceeds time. Print warning before actually logging. print 'Logging work.' # self.__log_holidays(sprint_dates) # self.__log_leaves() self.__log_daily_work(dates) # self.__log_meetings() # self.__log_sprint_meetings(sprint_dates) # self.__log_trainings() # self.__log_reviews() # self.__log_other_tasks() def __log_holidays(self, sprint_dates): holidays = helper.get_holidays_list() print 'Logging holidays...' for holiday in holidays: if sprint_dates[0] <= holiday <= sprint_dates[1]: worklog = self.jira.add_worklog(self.params['holidays_id'], '8h', started=parser.parse(holiday + 'T08:00:00-00:00'), comment=holidays[holiday]) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your holidays.') def __log_leaves(self): # TODO: Support for not whole day leaves print 'Logging your leaves...' for leave in self.params['leaves']: worklog = self.jira.add_worklog(leave['id'], leave['timeSpent'], started=parser.parse(leave['started'] + 'T08:00:00-00:00'), comment=leave['comment']) print worklog if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your leaves.') def __log_daily_work(self, dates): print 'Logging your daily tasks...' for task in self.params['daily_tasks']: for date in dates: worklog = self.jira.add_worklog(task['id'], task['timeSpent'], started=parser.parse(date + 'T08:00:00-00:00'), comment=task['comment']) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your daily work.') def __log_meetings(self): print 'Logging your meetings...' for meeting in self.params['meetings']: worklog = self.jira.add_worklog(meeting['id'], meeting['timeSpent'], started=parser.parse(meeting['started'] + 'T08:00:00-00:00'), comment=meeting['comment']) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your meetings.') def __log_sprint_meetings(self, sprint_dates): print 'Logging your sprint meetings...' for sprint_meeting in self.params['sprint_meetings']: worklog = worklog = self.jira.add_worklog(sprint_meeting['id'], sprint_meeting['timeSpent'], started=parser.parse(sprint_meeting['started'] + 'T08:00:00-00:00'), comment=sprint_meeting['comment']) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your sprint meetings.') def __log_trainings(self): print 'Logging your trainings...' for training in self.params['trainings']: worklog = self.jira.add_worklog(training['id'], training['timeSpent'], started=parser.parse(training['started'] + 'T08:00:00-00:00'), comment=training['comment']) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your trainings.') def __log_reviews(self): # TODO: Find a way to automate this print 'Logging your reviews...' for review in self.params['reviews']: worklog = self.jira.add_worklog(self.params('reviews_id'), '{}h'.format(.5 * len(reviews[review])), started=parser.parse(review + 'T08:00:00-00:00'), comment='\n'.join(reviews[review])) if not isinstance(worklog, int): raise RuntimeError('There was a problem logging your reviews.') def __log_other_tasks(self): # TODO: Make this a filler task function. print "Not yet supported"
class JiraClient: def __init__(self, email, token, server=SERVER): self.client = JIRA(server=server, basic_auth=(email, token), max_retries=MAX_RETRIES, timeout=4) def get_issues(self, start_at=0, query='', limit=ISSUES_COUNT): return self.client.search_issues( query, fields='key, summary, timetracking, status, assignee', startAt=start_at, maxResults=limit) def log_work(self, issue, time_spent, start_date, comment, adjust_estimate=None, new_estimate=None, reduce_by=None): self.client.add_worklog(issue=issue, timeSpent=time_spent, adjustEstimate=adjust_estimate, newEstimate=new_estimate, reduceBy=reduce_by, started=start_date, comment=comment) def get_possible_resolutions(self): resolutions = self.client.resolutions() possible_resolutions = [resolution.name for resolution in resolutions] return possible_resolutions def get_possible_versions(self, issue): all_projects = self.client.projects() current_project_key = issue.key.split('-')[0] for id, project in enumerate(all_projects): if project.key == current_project_key: current_project_id = id versions = self.client.project_versions( all_projects[current_project_id]) possible_versions = [version.name for version in versions] return possible_versions @staticmethod def get_remaining_estimate(issue): try: existing_estimate = issue.fields.timetracking.raw[ 'remainingEstimate'] except (AttributeError, TypeError, KeyError): existing_estimate = "0m" return existing_estimate @staticmethod def get_original_estimate(issue): try: original_estimate = issue.fields.timetracking.originalEstimate except JIRAError as e: return e.text except (AttributeError, TypeError): return "You should establish estimate first" return original_estimate def issue(self, key): return self.client.issue(key)
for i in range(0, len(issues)): #print (issues[i]) pos = i print "Task", pos + 1, ":", issues[i], ":", issues[i].fields.summary #print (" -> Name: ",issues[i].fields.summary) #print (" Status: ",issues[i].fields.status) print("==================================================") while 1: value = raw_input("You choose task number ? ") ID_JIRA = issues[int(value) - 1] print "=== ", issues[int(value) - 1].fields.summary, " ===" time = raw_input("Spent time (5h, 30m...): ") com = raw_input("Comment: ") print "You want to log work on: " print " 1. Today" print " 2. Other date" choice = raw_input("You choose 1 or 2 ? ") if int(choice) == 2: date = raw_input("Date (yyyy-mm-dd): ") date += "T17:20:00.000+0700" else: date = None yn = raw_input("You will continue to log work? (Y/N)? ") if yn == "Y": jira.add_worklog(ID_JIRA, timeSpent=time, comment=com, started=date) print("======================= Done =====================") elif yn == "N": print("===================== Canceled ===================") else: print("===================== Try again ==================")
)) jira_worklog.update( timeSpentSeconds=jira_worklog.timeSpentSeconds + shortfall) jira_worklog_total += shortfall if jira_worklog_total < time_entry_total: shortfall = time_entry_total - jira_worklog_total shortfall = math.ceil(shortfall / ROUND_SECONDS_TO) * ROUND_SECONDS_TO if jira_worklogs: jira_worklog = jira_worklogs[-1] print("Updating %s worklog %s to %.1g hrs" % ( jira_key, jira_worklog.started, (jira_worklog.timeSpentSeconds + shortfall) / 3600, )) jira_worklog.update( timeSpentSeconds=jira_worklog.timeSpentSeconds + shortfall) else: time_entry = time_entries[-1] print("Creating %s worklog %s of %.1g hrs" % (jira_key, time_entry["start"].strftime("%c"), shortfall / 3600)) jira.add_worklog( jira_key, timeSpentSeconds=shortfall, started=time_entry["start"], comment=input( "Summary: %s\nComment: " % time_entry["description"]) or None, )
class JiraHandler(): def __init__(self, settings): self.settings = settings # print(settings) options = {"server": self.settings["server"]} self.jira = JIRA(options, basic_auth=(self.settings["username"], self.settings["api_key"])) self.worklog_columns = [{ "header": "Issue", "field": "issue", "style": "cyan", "no_wrap": True }, { "header": "Started", "field": "started", "style": "green" }, { "header": "Comment", "field": "comment", "style": "magenta" }, { "header": "Time Spent", "field": "timeSpent", "justify": "right", "style": "green" }] s = json.dumps(self.settings["export_template"]) self.export_template = Template(s) def update(self, events): worklogs = [] for event in events: try: start_datetime = dateutil.parser.isoparse( event["start"]["dateTime"]) end_datetime = dateutil.parser.isoparse( event["end"]["dateTime"]) worklog = { "issue": event["extendedProperties"]["private"]["jira"], "timeSpent": td_format(end_datetime - start_datetime), "started": start_datetime } if "description" in event.keys( ) and event["description"] != None: worklog["comment"] = event["description"] else: worklog["comment"] = event["summary"] worklogs.append(worklog) except: pass if worklogs != []: pretty_print(worklogs, *self.worklog_columns) s = console.input(">[bold green]yes>[bold red]no>") if s == "yes": for worklog in worklogs: self.jira.add_worklog(worklog["issue"], comment=worklog["comment"], timeSpent=worklog["timeSpent"], started=worklog["started"]) print("The work items have been synched with Jira") else: print("No work has been logged for the requested day") def delete(self, *args): worklogs = [] worklogs_raw = [] (timeMin, timeMax) = calculate_time_span(*args) size = 100 initial = 0 while True: start = initial * size issues = self.jira.search_issues(self.settings['delete_jql'], start, size) if len(issues) == 0: break initial += 1 key = 1 for issue in issues: for worklog in self.jira.worklogs(issue): worklog_started = dateutil.parser.isoparse( worklog.raw["started"]) if timeMin.replace( tzinfo=timezone.utc) < worklog_started.replace( tzinfo=timezone.utc) < timeMax.replace( tzinfo=timezone.utc): worklogs.append(worklog) d = worklog.raw d['issue'] = issue.raw['key'] print(d) worklogs_raw.append(d) if worklogs_raw != []: pretty_print(worklogs_raw, *self.worklog_columns) console.print( "JIRA worklogs listed above will be deleted. This action cannot be undone" ) s = console.input(">[bold green]yes>[bold red]no>") if s == "yes": for worklog in worklogs: worklog.delete() print("The work items have been deleted") def export_issues(self): size = 100 initial = 0 issues_dict = {} while True: start = initial * size issues = self.jira.search_issues(self.settings['export_jql'], start, size) if len(issues) == 0: break initial += 1 key = 1 for issue in issues: # s = self.export_template.render(issue = issue) # print(s) # j = json.loads(s) issues_dict[issue.key] = { "summary": issue.fields.summary, "description": issue.fields.description, "extendedProperties": { "private": { "jira": issue.key, "project": "P4-18-4" } } } with open('jira_export.json', 'w') as f: json.dump(issues_dict, f)
class WorklogFiller(): """Fills out your worklog for the given date range for you""" def __init__(self): self.jira = False self.begin_date = begin_date self.end_date = end_date self.active_ticket_list = [] self.loginToJira() self.current_worklog = [] self.projects_to_cache = ('') self.cached_worklogs = {} def __init__(self, *args, **kwargs): ValueError.__init__(self, *args, **kwargs) def loginToJira(self): if username and password: self.jira = JIRA(server, basic_auth=(username, password)) def convertISO8601DateToDateTime(self, isotime): return dateutil.parser.parse(isotime) def convertToJQLDate(self, dttime): return dttime.strftime('"%Y/%m/%d"') def convertSeconds(self, time_in_seconds): return float(time_in_seconds) / 60.0 / 60.0 def convertSecondsToMinutes(self, time_in_seconds): return float(time_in_seconds) / 60.0 def convertWorkTime(self, seconds): return "".join( [str("%.0f" % self.convertSecondsToMinutes(seconds)), 'm']) def getWorkDayRange(self, begin=begin_date, end=end_date): time_delta = end - begin workdays = [ begin + datetime.timedelta(days=x) for x in range(0, time_delta.days) ] workdays = [workday for workday in workdays if workday.weekday() < 5] logging.debug("Dates: %s" % workdays) return workdays def getWorklog(self, issue): if issue.fields.project.key in self.projects_to_cache: if issue.key in self.cached_worklogs.keys(): logging.debug("Found cached worklog for %s" % issue.key) return self.cached_worklogs[issue.key] else: logging.debug( "No cache found for %s, requesting from Jira. This could take some time." % issue.key) self.cached_worklogs[issue.key] = self.jira.worklogs(issue) return self.cached_worklogs[issue.key] else: return self.jira.worklogs(issue) def getWorklogSumForIssueForDate(self, issue, date, user=username): # date must be a datetime object work_time_in_seconds_for_issue = 0.00 worklogs = self.getWorklog(issue) for worklog in worklogs: if worklog.author.key == user: wdate = self.convertISO8601DateToDateTime(worklog.started) if wdate.day == date.day and wdate.month == date.month and wdate.year == date.year: work_time_in_seconds_for_issue += worklog.timeSpentSeconds logging.info( "%d hours shown for %s on %s" % (self.convertSeconds(work_time_in_seconds_for_issue), issue, date)) return work_time_in_seconds_for_issue def getWorklogSumForDate(self, date): sum_seconds = 0 worklog_issues = self.jira.search_issues( 'worklogAuthor=%s and worklogDate="%s"' % (username, date.strftime('%Y/%m/%d')), maxResults=50) for issue in worklog_issues: logging.debug("Getting worklog sum for %s" % date) sum_seconds += self.getWorklogSumForIssueForDate(issue, date) return sum_seconds def getWorklogSumForDates(self, begin=begin_date, end=end_date, user=username): worklog_issues = self.jira.search_issues('worklogAuthor=%s and worklogDate >= "%s" and worklogDate <= "%s"' \ % (user, begin.strftime('%Y/%m/%d'), end.strftime('%Y/%m/%d')), maxResults=50) return self.getWorklogSumForTicketsInRange(worklog_issues, user, begin, end) def getWorklogSumForTicketsInRange(self, ticket_list, user=username, begin=begin_date, end=end_date): sum_seconds = 0.0 for issue in ticket_list: worklogs = self.getWorklog(issue) for worklog in worklogs: if worklog.author.key == user: if self.convertISO8601DateToDateTime(worklog.started) <= end and \ self.convertISO8601DateToDateTime(worklog.started) >= begin: sum_seconds += worklog.timeSpentSeconds return sum_seconds def getRemainingTimeForDate(self, date): remaining_seconds = 0 regular_day_seconds = 8 * 60 * 60 remaining_seconds = regular_day_seconds - self.getWorklogSumForDate( date) if remaining_seconds < 0: return 0 return remaining_seconds def getActiveTicketListForDates(self, begin=begin_date, end=end_date, user=username): active_tickets = self.jira.search_issues('assignee was not %s before "%s" AND assignee was %s before "%s"' \ % (user, begin.strftime('%Y/%m/%d'), user, end.strftime('%Y/%m/%d'))) if compatible_projects_keys: for index, ticket in enumerate(active_tickets): if ticket.fields.project.key not in compatible_projects_keys: logging.warning("Removed %s from active tickets" % ticket.key) active_tickets.pop(index) if not active_tickets: logging.info( 'No tickets found assigned during the given date period. Consider widening the range of dates to find tickets to assign time to. Exiting' ) sys.exit() return active_tickets def getWorkingDaysWithinDates(self, begin=begin_date, end=end_date): return workdays.networkdays(begin, end) def getTimeAllotment(self, begin=begin_date, end=end_date): leftover_seconds = (self.getWorkingDaysWithinDates(begin, end) * 8.0 * 60.0 * 60.0) - self.getWorklogSumForDates( begin, end) if leftover_seconds > 0: return leftover_seconds #/ 60.0 / 60.0 else: return 0.0 def addWorkLog( self, log_day, issue="", timeSpent="1h", ): # Must add timezone for JIRA api to accept d = log_day.replace(tzinfo=nyctz()) logging.info("Reporting %s time on issue %s for day %s" % (timeSpent, issue, d)) self.jira.add_worklog(issue=issue, timeSpent=timeSpent, started=d) def fillOutWorklogForMe(self, begin=begin_date, end=end_date): # Query for all tickets assigned between those dates my_active_tickets = self.getActiveTicketListForDates(begin, end) # Figure out total amount of hours left to be recorded total_seconds_unrecorded = self.getTimeAllotment() logging.debug("Total seconds to be logged %s" % total_seconds_unrecorded) time_per_ticket = total_seconds_unrecorded / len(my_active_tickets) logging.info("Work hours to be allocated to each ticket %s" % time_per_ticket) leftover_tickets = [] for ticket in my_active_tickets: leftover_tickets.append({ 'ticket': ticket, 'time': time_per_ticket }) logging.debug("Leftover tickets: %s" % leftover_tickets) date_range = self.getWorkDayRange(begin, end) for day in date_range: logging.debug("--> Working on day: %s <--" % day) day_is_full = False maxc = 0 while not day_is_full and maxc < 20: maxc += 1 logging.debug("Leftover tickets in loop: %s" % leftover_tickets) for index, leftover_ticket in enumerate(leftover_tickets): timeleft = self.getRemainingTimeForDate(day) logging.info("Time left in day: %s" % timeleft) if timeleft > 0: if leftover_ticket['time'] > timeleft: logging.debug( "Time left in %s exceeds time left in day" % leftover_ticket['ticket']) leftover_ticket['time'] -= timeleft self.addWorkLog(issue=leftover_ticket['ticket'],\ timeSpent=self.convertWorkTime(timeleft),\ log_day=day) day_is_full = True elif leftover_ticket['time'] == timeleft: logging.debug( "Time left in %s equals time left in day" % leftover_ticket['ticket']) leftover_tickets.pop(index) self.addWorkLog(issue=leftover_ticket['ticket'],\ timeSpent=self.convertWorkTime(timeleft),\ log_day=day) day_is_full = True elif leftover_ticket['time'] < timeleft: logging.debug( "Time left in %s is less than time left in day. Moving to next ticket" % leftover_ticket['ticket']) print leftover_ticket['time'] print self.convertWorkTime(leftover_ticket['time']) self.addWorkLog(issue=leftover_ticket['ticket'],\ timeSpent=self.convertWorkTime(leftover_ticket['time']),\ log_day=day) leftover_tickets.pop(index) else: break # self.getWorklogSumForTicketsInRange(my_active_tickets) def printIntro(self): printed = ( "Welcome to Worklogger.", "This script will 1)look for all tickets assigned to you within a give date range", "2) Figure out how much time left needs to be recorded", "3) Evenly distribute the time left in worklogs to be recorded among the tickets.", "\n", "It is HIGHLY recommended that you do two things first:", "1) Fill out your vacation and scrum times ", "2) Avoid running this script during peak Jira hours as it makes a LOT of queries\n", # "This script will attempt to do quite a bit of work for you. It will default to making", # "a dry run first, to prevent unwanted changes that might be annoying to fix.", "\n\n\n", ) print "\n".join(printed)
class JiraAPI: def __init__(self, config): self.config = config self.todo_status = self.config["status"]["todo"] self.in_progress_status = self.config["status"]["in_progress"] self.done_status = self.config["status"]["done"] self.project_id = self.config["project_id"] self.jira_api = JIRA(self.config["account_url"], basic_auth=(self.config["username"], self.config["token"])) self.current_issue = None self.get_current_issue() def get_current_issue(self): self.current_issue = next( (issue for issue in self.get_my_issues(self.in_progress_status)), None) return self.current_issue def get_issue(self, issue_key): return self.jira_api.issue(issue_key) def get_my_issues(self, status_id=None): """Get all my issues of filter by status. Args: status (str): Options are "todo", "in_progress" and "done" """ status_filter = "" if status_id: status_filter = " and status = {status}".format(status=status_id) query = ('assignee = currentUser() ' 'and project = {project_id}{status_filter} ' 'order by created desc').format(project_id=self.project_id, status_filter=status_filter) return list(self.jira_api.search_issues(query)) def change_status(self, issue, status_id): """Change an issue's status. Args: issue: <JIRA Issue> object status (str): Options are "todo", "in_progress" and "done" """ if issue.fields.status.id == status_id: return resolution_id = next(resolution["id"] for resolution in self.jira_api.transitions(issue) if resolution["to"]["id"] == status_id) info_log("Changing state of task {} to {}".format( issue, resolution_id)) self.jira_api.transition_issue(issue, resolution_id) def start_issue(self, issue): """Change issue state to 'in progress' and any other in progress to 'todo'. Args: issue: <JIRA Issue> object """ info_log("Starting task {}".format(issue)) for in_progress in self.get_my_issues(self.in_progress_status): if not in_progress.id == issue.id: self.change_status(in_progress, self.todo_status) self.change_status(issue, self.in_progress_status) def add_time(self, issue, seconds): """Add worklog time to a issue.""" seconds = max(int(seconds), 60) info_log("Add {} seconds to worklog to {}".format(seconds, issue)) self.jira_api.add_worklog(issue.id, adjustEstimate="auto", timeSpentSeconds=seconds)