class TogglPyTests(unittest.TestCase): def setUp(self): self.api_key = os.environ['TOGGL_API_KEY'] if self.api_key is None: raise Exception("Unable to execute api tests without an api key") self.workspace_id = os.environ['WORKSPACE_ID'] if self.workspace_id is None: raise Exception("Unable to execute api tests without a workspace key to query") self.toggl = Toggl() self.toggl.setAPIKey(self.api_key) def test_connect(self): response = self.toggl.request("https://www.toggl.com/api/v8/clients") self.assertTrue(response is not None) def test_putTimeEntry(self): request_args = { 'workspace_id': self.workspace_id, } entries = self.toggl.getDetailedReport(request_args) #for this tests I'm tagging my Pomodoro Entries missing_projects = [r for r in entries['data'] if r['project'] is None and 'Pomodoro' in r['description'] ] me = missing_projects[0] me_id = me['id'] #remember for later #I've tagged my pomodoro entries as Self/Self cp = self.toggl.getClientProject("Self", "Self") project_id = cp['data']['id'] me['pid'] = project_id #his is the new stuff response = self.toggl.putTimeEntry({"id": me_id, "pid":project_id}) self.assertTrue(response is not None) self.assertTrue('data' in response) self.assertTrue(response['data']['pid'] == project_id) def test_getDetailedReportCSV(self): data = { 'workspace_id': self.workspace_id, } csvfile = 'data.csv' self.toggl.getDetailedReportCSV(data, csvfile) self.assertTrue(os.path.isfile(csvfile)) os.remove(csvfile) data = self.toggl.getDetailedReportCSV(data) self.assertTrue(data is not None) def test_getDetailedReport(self): data = { 'workspace_id': self.workspace_id, } d = self.toggl.getDetailedReport(data) self.assertTrue(d is not None) self.assertTrue(len(d.keys()) > 0 ) fields = ['total_count', 'total_currencies', 'total_billable', 'data'] for f in fields: self.assertTrue(f in d.keys()) data = d['data'] self.assertTrue(len(data)>0) dr = data[0] self.assertTrue('client' in dr) self.assertTrue('start' in dr) self.assertTrue('end' in dr) self.assertTrue('task' in dr) self.assertTrue('user' in dr) self.assertTrue('project' in dr)
class TogglPyTests(unittest.TestCase): def setUp(self): self.api_key = os.environ['TOGGL_API_KEY'] if self.api_key is None: raise Exception("Unable to execute api tests without an api key") self.workspace_id = os.environ['WORKSPACE_ID'] if self.workspace_id is None: raise Exception( "Unable to execute api tests without a workspace key to query") self.toggl = Toggl() self.toggl.setAPIKey(self.api_key) def test_connect(self): response = self.toggl.request( "https://api.track.toggl.com/api/v8/clients") self.assertTrue(response is not None) def test_putTimeEntry(self): request_args = { 'workspace_id': self.workspace_id, } entries = self.toggl.getDetailedReport(request_args) #for this tests I'm tagging my Pomodoro Entries missing_projects = [ r for r in entries['data'] if r['project'] is None and 'Pomodoro' in r['description'] ] me = missing_projects[0] me_id = me['id'] #remember for later #I've tagged my pomodoro entries as Self/Self cp = self.toggl.getClientProject("Self", "Self") project_id = cp['data']['id'] me['pid'] = project_id #his is the new stuff response = self.toggl.putTimeEntry({"id": me_id, "pid": project_id}) self.assertTrue(response is not None) self.assertTrue('data' in response) self.assertTrue(response['data']['pid'] == project_id) def test_getDetailedReportCSV(self): data = { 'workspace_id': self.workspace_id, } csvfile = 'data.csv' self.toggl.getDetailedReportCSV(data, csvfile) self.assertTrue(os.path.isfile(csvfile)) os.remove(csvfile) data = self.toggl.getDetailedReportCSV(data) self.assertTrue(data is not None) def test_getDetailedReport(self): data = { 'workspace_id': self.workspace_id, } d = self.toggl.getDetailedReport(data) self.assertTrue(d is not None) self.assertTrue(len(d.keys()) > 0) fields = ['total_count', 'total_currencies', 'total_billable', 'data'] for f in fields: self.assertTrue(f in d.keys()) data = d['data'] self.assertTrue(len(data) > 0) dr = data[0] self.assertTrue('client' in dr) self.assertTrue('start' in dr) self.assertTrue('end' in dr) self.assertTrue('task' in dr) self.assertTrue('user' in dr) self.assertTrue('project' in dr)
class TogglReportingApp(tk.Tk): def __init__(self, *args, **kwargs): self.toggl_setup() self.tkinter_setup(*args, **kwargs) def toggl_setup(self): self.connect_to_toggl() self.get_toggl_project_data() self.get_toggl_report_data() self.define_preset_date_bounds() self.group_by = 'Project' self.description_groupings = [] self.date_bounds = {} def tkinter_setup(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) tk.Tk.wm_title(self, "Toggl Reporting") container = ttk.Frame(self) container.pack(side="top", fill="both", expand=True) container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(0, weight=1) self.frames = {} frame = StartPage(container, self) self.frames[StartPage] = frame frame.grid(row=0, column=0, sticky='nsew') """ for F in (StartPage): frame = F(container, self) self.frames[F] = frame frame.grid(row=0, column=0, sticky="nsew") """ self.show_frame(StartPage) def show_frame(self, cont): frame = self.frames[cont] frame.tkraise() # Establish the connection to the TogglAPI, and collect the project data. def connect_to_toggl(self): self.toggl = Toggl() self.toggl.setAPIKey(config.API_KEY) # Get information about the user's projects from Toggl def get_toggl_project_data(self): self.user_data = self.toggl.request( "https://www.toggl.com/api/v8/me?with_related_data=true") project_data = self.remove_empty_projects( self.user_data['data']['projects']) project_data_dict = { project_data[i]['name']: project_data[i] for i in range(0, len(project_data)) } self.master_project_list = project_data_dict # Unchanging "master" list client_data = self.user_data['data']['clients'] client_data_dict = { client_data[i]['name']: client_data[i] for i in range(0, len(client_data)) } self.master_client_list = client_data_dict # Unchanging "master" list self.client_list = client_data_dict # List of active projects to be displayed in the graph # Assigning clients to projects. for client in client_data: client_id = client['id'] client_name = client['name'] for project_name in self.master_project_list: if not 'cid' in self.master_project_list[project_name]: self.master_project_list[project_name]['client'] = False elif self.master_project_list[project_name][ 'cid'] == client_id: self.master_project_list[project_name][ 'client'] = client_name #self.project_list = list(self.master_project_list.keys()) self.project_list = [] # Return a version of the project list, where all projects without any tracked hours are removed. # (This also removes projects which have been deleted via Toggl, but are still retrieved via the API) def remove_empty_projects(self, project_list): to_delete = [] for i in range(0, len(project_list)): if not 'actual_hours' in project_list[i]: to_delete.append(i) for i in sorted(to_delete, reverse=True): del project_list[i] return project_list def get_toggl_report_data(self): years = 1 date_bounds = { 'start': datetime.now() - timedelta(days=365 * years), #Get reports from the last n years. 'end': datetime.now() } split_bounds = self.split_date_bounds(date_bounds) # A list of all the reports we gather from Toggl. (Max 1 year each) reports = [] for bounds in split_bounds: params = {'start': bounds['start'], 'end': bounds['end']} reports.append(self.get_report(params)) self.full_toggl_report = self.join_reports(reports) def define_preset_date_bounds(self): year = 365 month = 30 self.preset_date_bounds = { 'Past Week': 7, 'Past Month': month, 'Past 6 Months': month * 6, 'Past Year': year, 'Past 2 Years': year * 2, 'Past 5 Years': year * 5, 'Custom': 0 } def set_group_by(self, group_by): self.group_by = group_by # Make a request to Toggl to get a report of all data from a given time frame. def get_report(self, params): data = { 'workspace_id': config.WORKSPACE_ID, 'since': params['start'], 'until': params['end'], } return self.toggl.getDetailedReportCSV(data) def is_included_project(self, project): if project in self.project_list: return True else: return False def is_included_client(self, client): if client in self.client_list: return True else: return False def main_sequence(self): self.update_project_data() self.update_client_data() self.update_description_restrictions() report = self.full_toggl_report day = self.get_day() day = self.populate_day(day, report) self.create_graph(day) def update_project_data(self): user_selection = self.project_list self.project_list = {} for project_name in user_selection: self.project_list[project_name] = self.master_project_list[ project_name] def update_client_data(self): user_selection = self.client_list self.client_list = {} for client_name in user_selection: self.client_list[client_name] = self.master_client_list[ client_name] # Return a list containing a series of bounds, each of at most 1 year long. def split_date_bounds(self, bounds): span = self.get_report_span(bounds['start'], bounds['end']) number_of_years = math.ceil(span / 365) bounds = [] for year in range(number_of_years): remaining_days_after_subtraction = span - 365 start = datetime.now() - timedelta(days=span) if (remaining_days_after_subtraction > 0): span = remaining_days_after_subtraction end = datetime.now() - timedelta( days=span + 1 ) #(Plus 1 so we don't get an overlap at the edge of the bounds) else: end = datetime.now() bounds.append({'start': start, 'end': end}) bounds.reverse( ) # We reverse the order, because we need reverse-chronological in order to signal when to stop the populate_day function. return bounds # Return the length of time that a report covers. (In days) def get_report_span(self, start, end): span = end - start return span.days # Join the given reports together, saving them as a temporary csv file. def join_reports(self, reports_list): temporary_csv_file = tempfile.NamedTemporaryFile() for report in reports_list: with open(temporary_csv_file.name, 'ab') as csv: csv.write(report) return temporary_csv_file def update_description_restrictions(self): self.allowed_descriptions = [] self.excluded_descriptions = [] for i in self.description_search_list: description_row = self.description_search_list[i] value = description_row['entry'].get().lower() excluded = bool(description_row['exclude_checkbox_value'].get()) if excluded: self.excluded_descriptions.append(value) else: self.allowed_descriptions.append(value) # If there are no specifically allowed descriptions, say that an empty string is allowed. (Otherwise we'll return nothing). if not self.allowed_descriptions: self.allowed_descriptions = [''] # Return an dictionary containing projects with minutes set to zero. def get_day(self): minutesInDay = ( 60 * 24 ) - 1 #Subtract 1 because it is zero indexed. Minute 1440 is the following midnight, which will be empty. day = {} emptyDay = {} for i in range(0, minutesInDay + 1): emptyDay[i] = 0 if self.group_by == 'Project': for project in self.project_list: day[project] = emptyDay.copy() if self.group_by == 'Description': for grouping in self.description_groupings: day[grouping['title']] = emptyDay.copy() if self.group_by == 'Timeframe': for timeframe in self.date_bounds.values(): day[timeframe['name']] = emptyDay.copy() if self.group_by == 'None': day = emptyDay return day # Return the earliest date from a set of timeframes. def find_earliest_date_bound(self, timeframes): earliest = datetime.today() for date_bounds_pair in self.date_bounds.values(): start = date_bounds_pair['start'] if start < earliest: earliest = start return earliest # Populate the day dictionary with data from the report. def populate_day(self, day, report): earliest_date_bound = self.find_earliest_date_bound(self.date_bounds) with open(report.name, 'r') as file: reader = csv.DictReader(file) for row in reader: print(row) try: entry_date = datetime.strptime(row['Start date'], '%Y-%m-%d') except ValueError: continue if entry_date < earliest_date_bound: # If the entry is earlier than our earliest date bound, we stop. break print(row) project = row['Project'] description = row['Description'] client = row['Client'] # Skipping header rows from merged csv. if row['Email'] == 'Email': continue if not self.is_included_project(project): continue if not self.is_included_client(client): continue matched_timeframes = [] # Skipping entries which are outside of our date bounds. within_bounds = False for date_bounds_pair in self.date_bounds.values(): if date_bounds_pair[ 'start'] <= entry_date <= date_bounds_pair['end']: within_bounds = True if self.group_by == 'Timeframe': timeframe_name = date_bounds_pair['name'] if not timeframe_name in matched_timeframes: matched_timeframes.append(timeframe_name) if not within_bounds: continue description_match = False if self.group_by == 'Description': matched_description_groups = [] for description_grouping in self.description_groupings: for user_description in description_grouping[ 'descriptions']: if user_description in description.lower(): description_match = True grouping_title = description_grouping['title'] if not grouping_title in matched_description_groups: matched_description_groups.append( grouping_title) else: for allowed_description in self.allowed_descriptions: if allowed_description in description.lower(): description_match = True break for excluded_description in self.excluded_descriptions: if excluded_description in description.lower(): description_match = False if not description_match: continue startMinutes = self.getTimeInMinutes(row['Start time']) duration = self.getTimeInMinutes(row['Duration']) for i in range(startMinutes + 1, startMinutes + duration + 1): targetMinute = i if targetMinute >= 1440: targetMinute = abs(1440 - i) if self.group_by == 'Project': day[project][targetMinute] += 1 elif self.group_by == 'Description': for description_group in matched_description_groups: day[description_group][targetMinute] += 1 elif self.group_by == 'Timeframe': for timeframe in matched_timeframes: day[timeframe][targetMinute] += 1 elif self.group_by == 'None': day[targetMinute] += 1 day = self.remove_empty_categories(day) return day # Remove all the categories from a day which have zero minutes tracked over the time span. def remove_empty_categories(self, day): empty_categories = [] for category_name in day: if all(value == 0 for value in day[category_name].values()): empty_categories.append(category_name) for category_name in empty_categories: del day[category_name] return day def getTimeInMinutes(self, time): split = re.split(':', time) hours = int(split[0]) minutes = int(split[1]) minutes += 60 * hours return minutes def get_minutes_since_midnight(self): now = datetime.now() midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) minutes = (now - midnight).seconds / 60 return minutes def create_graph(self, day): if self.group_by == 'Project': for project_name in day: hex_color = self.project_list[project_name]['hex_color'] plt.plot(list(day[project_name].keys()), list(day[project_name].values()), label=project_name, color=hex_color) elif self.group_by == 'Description': for grouping in self.description_groupings: title = grouping['title'] plt.plot(list(day[title].keys()), list(day[title].values()), label=title) elif self.group_by == 'Timeframe': for timeframe_name in day: plt.plot(list(day[timeframe_name].keys()), list(day[timeframe_name].values()), label=timeframe_name) elif self.group_by == 'None': plt.plot(list(day.keys()), list(day.values())) plt.ylabel('Frequency') positions = [] labels = [] for i in range(0, 24): positions.append(i * 60) time = str(i) + ':00' if len(time) < 5: time = '0' + time labels.append(time) if self.highlight_current_time.get(): minutes_since_midnight = self.get_minutes_since_midnight() plt.axvline(x=minutes_since_midnight) plt.xticks(positions, labels) plt.grid() plt.legend() mng = plt.get_current_fig_manager() mng.full_screen_toggle() plt.show()