예제 #1
0
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']))
예제 #2
0
파일: timelogs.py 프로젝트: skarbot/utils
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')
예제 #3
0
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()
예제 #4
0
파일: zup.py 프로젝트: johannfr/zup
 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)
예제 #6
0
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"
        )
예제 #7
0
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')
예제 #8
0
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'
예제 #9
0
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)
예제 #10
0
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.')
예제 #11
0
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())
예제 #12
0
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]
            }
        })
예제 #13
0
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)
예제 #14
0
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"
예제 #15
0
  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')
예제 #16
0
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.')
예제 #17
0
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))
예제 #18
0
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
예제 #19
0
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"
예제 #20
0
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 ==================")
예제 #22
0
            ))
            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,
            )
예제 #23
0
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)
예제 #25
0
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)