class Toggls(): def __init__(self, key): self.toggl = Toggl() self.toggl.setAPIKey(key) def get_Workspaces(self): return self.toggl.getWorkspaces() def getprojectsinworkspace(self, id): urls = "https://www.toggl.com/api/v8/workspaces/" + str(id) + "/projects" return self.toggl.request(urls) def get_workspace(self, name): return self.toggl.getWorkspace(name) def make_time_entry(self, pid, workout): start = workout[1][6:10] + "-" + workout[1][:2] + "-" + workout[1][3:5] + "T" + workout[1][12:] + ".000Z" time = float(workout[2][0])*60*60 + float(workout[2][2:4])*60 + float(workout[2][5:7]) url = 'https://www.toggl.com/api/v8/time_entries' data = { "time_entry": { "description":"Workout", "tags":[""], "duration": time, "start": start, "pid": pid, "created_with":"api" } } return self.toggl.postRequest(url, parameters=data)
def _loadActiviy(self): toggl = Toggl() toggl.setAPIKey(self._token) self._report_date = self._daily_date - timedelta(1) self.friday_text = '' if self._report_date.isoweekday() is 6 or self._report_date.isoweekday() is 7: self._report_date = self._daily_date - timedelta(3 if self._daily_date.isoweekday() is 1 else 2) self.friday_text = " (Friday)" self.report_day = '{0:%Y-%m-%d}'.format(self._report_date) me_info = toggl.request(toggl_urls.me) self._username = me_info['data']['fullname'] summary_url = toggl_urls.summary + "?workspace_id={}&since={}&until={}&user_agent=api_test".format( self._workspace_id, self.report_day, self.report_day) response = toggl.request(summary_url) activity = {} default_project = conf_manager.get_default_project() for data in response['data']: project_name = default_project if None == data['title']['project'] else data['title']['project'] activity[project_name] = [] for item in data['items']: hours = self.millis_to_hours(item['time']) self._total_hours += hours activity[project_name].append(Task(item['title']['time_entry'], project_name, hours).serialize()) self._activity = activity return activity
def toggl_get_tasks(token=None) -> List[TglTask]: toggl = Toggl() if token is None: token = os.getenv("TOGGL_API_TOKEN") toggl.setAPIKey(token) workspaces = [] for e in _retry(lambda: toggl.getWorkspaces()): workspaces.append((e["id"], e["name"])) projects = [] for wid, wname in workspaces: for e in _retry(lambda: toggl.request(Endpoints.WORKSPACES + f"/{wid}/projects")) or []: projects.append((e["id"], e["name"])) ptasks = [] for pid, pname in projects: for e in _retry(lambda: toggl.getProjectTasks(pid)) or [{ "id": 0, "name": "" }]: ptasks.append(TglTask(pid, pname, e["id"], e["name"])) return ptasks
def toggl_delete_entries_for_ids(ids, token=None): toggl = Toggl() if token is None: token = os.getenv("TOGGL_API_TOKEN") toggl.setAPIKey(token) for id_ in ids: endpoint = Endpoints.TIME_ENTRIES + f"/{id_}" toggl_http_request(toggl, endpoint, "DELETE")
class Toggl2GSuiteTest(unittest.TestCase): def setUp(self): self.api_key = os.environ["TOGGL_API_KEY"] self.toggl = Toggl() self.toggl.setAPIKey(self.api_key) # see https://stackoverflow.com/questions/19153462/get-excel-style-column-names-from-column-number LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @staticmethod def excel_style(row, col): """ Convert given row and column number to an Excel-style cell name. """ result = [] while col: col, rem = divmod(col - 1, 26) result[:0] = Toggl2GSuiteTest.LETTERS[rem] return "".join(result) + str(row) def test_toggl2gsuite(self): # have to do this year by year data = { "workspace_id": os.environ["WORKSPACE_ID"], } y = self.toggl.getDetailedReport(data) credentials = ServiceAccountCredentials.from_json_keyfile_name( os.environ["KEYFILE"], ["https://spreadsheets.google.com/feeds"] ) client = gspread.authorize(credentials) sheet = client.open_by_url(os.environ["SHEET_URL"]) worksheet = sheet.get_worksheet(0) wrote_header = False columns_to_write = [ "user", "updated", "start", "end", "client", "project", "description", "is_billable", "billable", ] cell_row = 0 for row_idx, rec in enumerate(y["data"]): if wrote_header == False: for col_idx, header in enumerate(columns_to_write): worksheet.update_acell( Toggl2GSuiteTest.excel_style(row_idx + 1, col_idx + 1), header ) wrote_header = True for col_idx, header in enumerate(columns_to_write): worksheet.update_acell( Toggl2GSuiteTest.excel_style(row_idx + 2, col_idx + 1), rec[header] )
def _get_toggl_client(api_key): client = Toggl() client.setAPIKey(api_key) try: client.getWorkspaces() except HTTPError: raise click.BadParameter('Invalid api key or connection problem') return client
def main(): # Read config file config = read_config_file() api_key = config.get('general', 'api_key').strip() api = Toggl() api.setAPIKey(api_key) projects = download_projects(api) dump_projects_as_csv(projects, sys.stdout)
def toggl_add_entries(entries: List[TglStandardEntry], tasks: List[TglTask], token=None): toggl = Toggl() if token is None: token = os.getenv("TOGGL_API_TOKEN") toggl.setAPIKey(token) taskmap = {} for t in tasks: k = t.project_name, t.task_name v = t.project_id, t.task_id taskmap[k] = v togglentries = [] for i, entry in enumerate(entries, start=1): try: try: pid, tid = taskmap[(entry.project, entry.task)] except KeyError: raise ConvertingError( f"{entry.project}, {entry.task} not found") togglentry = { "description": entry.description, "pid": pid, "tid": tid if tid else None, "start": entry.start.astimezone().isoformat(), "duration": entry.duration.seconds, "created_with": "easytrack", } togglentries.append(togglentry) except ConvertingError as e: e.i = i e.line = e raise for i, togglentry in enumerate(togglentries, start=1): try: try: print(i, '/', len(togglentries)) time.sleep(2) resp = toggl.postRequest(Endpoints.TIME_ENTRIES, {"time_entry": togglentry}) except urllib.error.HTTPError as e: raise TogglError(body=e.fp.read().decode(), underlying=e) resp = toggl.decodeJSON(resp) if resp.get("data") is None: raise ConvertingError(f"data not found in resp {resp}") except ConvertingError as e: e.i = i e.line = e raise
def main(self): API_TOKEN = certification.load_json() if not API_TOKEN: print('[WARN]:Please set the API token in config.json') toggl = Toggl() toggl.setAPIKey(API_TOKEN) args = argument.get_argument() if args.function.lower() == 'start': workspace_id = self.get_workspace_id(toggl) self.get_project(toggl, workspace_id) self.start(toggl) elif args.function.lower() == 'stop': self.stop(toggl)
def get_data(self): toggl = Toggl() toggl.setAPIKey(self.config['api']) data = { 'workspace_id': workspace_id, 'user_agent': user_agent, 'since': self.last_month_first, 'until': self.last_month_last } self.response = toggl.request( "https://toggl.com/reports/api/v2/details", parameters=data) self.save_raw_data()
def team_weekly_report(destination, since, until, api_key, workspace): logger.info( "Downloading the team weekly report from {} until {} into {}".format( since, until, destination)) toggl = Toggl() toggl.setAPIKey(api_key) data = { 'workspace_id': workspace, # see the next example for getting a workspace id 'since': since, 'until': until } try: result = toggl.getSummaryReport(data) except Exception as e: logger.error("Unable to download the team weekly data {}".format(e)) return # Calculate hours and minutes total_ms = result['total_grand'] if total_ms: hours, minutes = divmod(total_ms / 1000 / 60, 60) time_str = "Total team hours: {:.0f}h {:.0f}m".format(hours, minutes) else: time_str = "Total team hours: No hours recorded" # Find all project worked on items_worked_on = [ item["title"]["time_entry"] for project in result["data"] for item in project["items"] ] if len(items_worked_on) == 0: items_worked_on = ["No tasks worked on for this time period"] # Calculate the pretty data for the start of the week date = datetime.strptime(since, "%Y-%m-%d") formatted_week = date.strftime("%B %d") formatted_items = "- " + "\n- ".join(items_worked_on) formatted_team_report = team_report_template.format( formatted_week, formatted_items, time_str) logger.info("Created team report:") logger.info(formatted_team_report) logger.info("Adding to team log file %s", destination) with open(destination, "a") as report: report.write(formatted_team_report) logger.info("Done team report")
def toggl_get_entries_raw(date: datetime.date, token=None): toggl = Toggl() if token is None: token = os.getenv("TOGGL_API_TOKEN") toggl.setAPIKey(token) start_date = datetime.datetime.combine(date, datetime.time()).astimezone() params = {"start_date": start_date.isoformat()} params["end_date"] = (start_date + datetime.timedelta(days=1) + datetime.timedelta(minutes=-1)).isoformat() print(date) time.sleep(2) togglentries = toggl.request(Endpoints.TIME_ENTRIES, params) return togglentries
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")
class Toggl2GSuiteTest(unittest.TestCase): def setUp(self): self.api_key = os.environ['TOGGL_API_KEY'] self.toggl = Toggl() self.toggl.setAPIKey(self.api_key) # see https://stackoverflow.com/questions/19153462/get-excel-style-column-names-from-column-number LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' @staticmethod def excel_style(row, col): """ Convert given row and column number to an Excel-style cell name. """ result = [] while col: col, rem = divmod(col - 1, 26) result[:0] = Toggl2GSuiteTest.LETTERS[rem] return ''.join(result) + str(row) def test_toggl2gsuite(self): # have to do this year by year data = { 'workspace_id': os.environ['WORKSPACE_ID'], } y = self.toggl.getDetailedReport(data) credentials = ServiceAccountCredentials.from_json_keyfile_name( os.environ['KEYFILE'], ['https://spreadsheets.google.com/feeds']) client = gspread.authorize(credentials) sheet = client.open_by_url(os.environ['SHEET_URL']) worksheet = sheet.get_worksheet(0) wrote_header = False columns_to_write = ['user', 'updated', 'start', 'end', 'client', 'project', 'description', 'is_billable', 'billable'] cell_row = 0 for row_idx, rec in enumerate(y['data']): if wrote_header == False: for col_idx, header in enumerate(columns_to_write): worksheet.update_acell(Toggl2GSuiteTest.excel_style(row_idx + 1, col_idx + 1), header) wrote_header = True for col_idx, header in enumerate(columns_to_write): worksheet.update_acell(Toggl2GSuiteTest.excel_style(row_idx + 2, col_idx + 1), rec[header])
def user_weekly_report(directory, since, until, api_key, users, workspace): """ Downloads each users weekly reports as a summary and details pdf into their own directories. :param workspace: :param api_key: The toggl api key to use. :param users: The dictionary are users with the key as the user id and the values as the full name. :param directory: The root destination directory. Each user will have a sub-directory created for their reports :param since: The start date in the form yyyy-MM-dd :param until: the end data in the form yyyy-MM-dd. Files will be prepended with this date string. :return: None """ logging.info( "Downloading user weekly reports from {} until {} into {}".format( since, until, directory)) toggl = Toggl() toggl.setAPIKey(api_key) for uid, name in users.items(): logger.info("Downloading reports for {}".format(name)) name = users[uid] data = { 'workspace_id': workspace, # see the next example for getting a workspace id 'since': since, 'until': until, 'user_ids': uid } folder = path.join(directory, name) if not path.exists(folder): logger.info("Creating the folder {}".format(folder)) makedirs(folder) details = path.join(folder, until + "-details.pdf") summary = path.join(folder, until + "-summary.pdf") try: toggl.getDetailedReportPDF(data, details) logger.info("Downloaded {}".format(details)) toggl.getSummaryReportPDF(data, summary) logger.info("Downloaded {}".format(summary)) except Exception as e: logging.error(e)
def get_entries(start, end, from_workspace_id, from_user_id): toggl = Toggl() toggl.setAPIKey(os.getenv('TOGGL_APIKEY')) response = toggl.request( f'https://toggl.com/reports/api/v2/details?workspace_id={from_workspace_id}&since={start}&until={end}&user_agent={user_agent}&uid={from_user_id}&include_time_entry_ids=true&user_ids={from_user_id}' ) if response['total_count'] > response['per_page']: raise Exception('Paging not supported, results exceed the page') for k, v in response.items(): if k != 'data': print(k, v) entries = [] for time_entry in response['data']: print(time_entry) entries.append(time_entry) return entries
def get_projects(): """Since the current project function in PyToggl doesn't work, I made my own hacky version. Gets all projects assigned to a client and returns it as a list""" toggl = Toggl() toggl.setAPIKey(TOGGL_TOKEN) clients = toggl.getClients() toggl_projects = [] for client in clients: projects = toggl.getClientProjects(client["id"]) if projects != None: for project in projects: toggl_projects.append(project) return toggl_projects
def save_entries(entries, workspace_id, user_id, project_id, hourdiff_to_utc): toggl = Toggl() toggl.setAPIKey(os.getenv('TOGGL_APIKEY')) for entry in entries: start = entry['start'] start_date = datetime.datetime.strptime(start, '%Y-%m-%dT%H:%M:%S%z') y = start_date.year m = start_date.month d = start_date.day h = start_date.hour desc = entry['description'] print(entry['dur']) toggl.createTimeEntry(round(entry['dur'] / 3600000.0, 1), description=desc, projectid=project_id, year=y, month=m, day=d, hour=h, hourdiff=hourdiff_to_utc)
class TogglApi: def __init__(self): self.config = load_config() api_token = self.config['toggl']['token'] self.toggl = Toggl() self.toggl.setAPIKey(api_token) def preset_projects(self): return self.config['toggl']['preset_projects'] def current_timer(self): timer = self.toggl.currentRunningTimeEntry()['data'] if timer: if 'pid' in timer: project = self.get_project_name(timer['pid']) else: project = ['Kein Projekt', (244, 244, 6)] timer_dic = { 'name': timer.get('description', ""), 'id': timer['id'], 'start_time': datetime.fromtimestamp(int(timer['duration']) * -1), 'project_name': project[0], 'project_color': project[1] } # start = datetime.strptime(timer['start'],"%Y-%m-%dT%H:%M:%S+00:00") return timer_dic else: return { 'name': "", 'id': "1234", 'start_time': datetime.fromtimestamp(1542385078), 'project_name': "Kein Projekt", 'project_color': (30, 0, 0) } def get_project_name(self, pid): project = self.toggl.getProject(pid)['data'] name = project['name'] try: color = self.hex_to_rgb(project['hex_color']) color = (int(color[0] / 2), int(color[1] / 2), int(color[2] / 2)) except (): color = (200, 200, 200) return name, color def start_timer(self, project_name, description): print(self.config['toggl']['project_ids'][project_name]) self.toggl.startTimeEntry( description, self.config['toggl']['project_ids'][project_name]) def stop_timer(self): current_timer_id = self.current_timer()['id'] if current_timer_id: self.toggl.stopTimeEntry(current_timer_id) return True return False @staticmethod def hex_to_rgb(hex_str): hex = hex_str[1:] # remove pound sign # https://stackoverflow.com/questions/29643352/converting-hex-to-rgb-value-in-python return tuple(int(hex[i:i + 2], 16) for i in (0, 2, 4))
def main(args): pp = pprint.PrettyPrinter(indent=4) # create a Toggl object and set our API key toggl_account = Toggl() toggl_account.setAPIKey(args.toggl_key) toggl_tz_str = toggl_account.request( "https://www.toggl.com/api/v8/me")['data']['timezone'] toggl_tz = pytz.timezone(toggl_tz_str) # figure out what ranges to sync for if args.days: edate = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) sdate = edate - timedelta(days=abs(args.days) + 1) elif args.daterange: dates = [dateparser.parse(x) for x in args.daterange] if len(dates) < 2: dates.append(datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)) dates = sorted(dates) sdate = dates[0] edate = dates[1] + timedelta(days=1) else: edate = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) sdate = edate + timedelta(days=-365) sdate_aw = toggl_tz.localize(sdate) edate_aw = toggl_tz.localize(edate) # do some fancy date windowing required for retrieving tasks from toggl toggl_dateranges = [] chunks = (edate - sdate).days // 180 partials = (edate - sdate).days % 180 for i in range((edate - sdate).days // 180): toggl_dateranges.append([ sdate + timedelta(days=i * 180), sdate + timedelta(days=(i + 1) * 180 - 1) ]) if partials: toggl_dateranges.append([ sdate + timedelta(days=chunks * 180), sdate + timedelta(days=chunks * 180 + partials) ]) toggl_dateranges = [[toggl_tz.localize(dr[0]), toggl_tz.localize(dr[1])] for dr in toggl_dateranges] # collect toggl entries toggl_entries = [] toggl_clients = toggl_account.getClients() toggl_workspaces = toggl_account.getWorkspaces() toggl_projects = [] for client in toggl_clients: toggl_projects = toggl_projects + toggl_account.getClientProjects( client['id']) for wid in [w['id'] for w in toggl_workspaces]: for dr in toggl_dateranges: entries_left = 1 page = 1 while entries_left > 0: entries = toggl_account.request( Endpoints.REPORT_DETAILED, { 'workspace_id': wid, 'user_agent': 'https://github.com/a3ng7n/timesheet-sync', 'page': page, 'since': dr[0], 'until': dr[1] }) toggl_entries = entries['data'] + toggl_entries entries_left = entries['total_count'] - entries[ 'per_page'] if page == 1 else entries_left - entries[ 'per_page'] page += 1 task_names = [{ 'id': str(x['pid']) + x['description'], 'pid': x['pid'], 'description': x['description'] } for x in toggl_entries] toggl_task_names = list({x['id']: x for x in task_names}.values()) toggl_task_names = sorted(toggl_task_names, key=lambda k: k['pid'] if k['pid'] else 0) for i, t in enumerate(toggl_task_names): t['id'] = i # collect harvest entries harvest_account = harvest.Harvest(uri=args.harvest_url, account_id=args.harvest_account_id, personal_token=args.harvest_key) try: harvest_user_id = [ x['user']['id'] for x in harvest_account.users if x['user']['email'] == args.harvest_email ].pop() except IndexError: print("Could not find user with email address: {0}".format( args.harvest_email)) raise tasks = [] harvest_entries = [] harvest_clients = harvest_account.clients() for client in harvest_clients: harvest_projects = harvest_account.projects_for_client( client['client']['id']) for project in harvest_projects: some_entries = harvest_account.timesheets_for_project( project['project']['id'], start_date=sdate_aw.isoformat(), end_date=edate_aw.isoformat()) for e in some_entries: e['day_entry']['client_id'] = [ y['project']['client_id'] for y in harvest_projects if y['project']['id'] == e['day_entry']['project_id'] ].pop() harvest_entries = harvest_entries + some_entries tasks = tasks + [{ **x['task_assignment'], 'client_id': client['client']['id'] } for x in harvest_account.get_all_tasks_from_project( project['project']['id'])] task_names = [{**x, 'id': str(x['client_id'])\ + str(x['project_id'])\ + str(x['task_id'])} for x in tasks] harvest_task_names = list({x['id']: x for x in task_names}.values()) harvest_task_names = sorted(harvest_task_names, key=lambda k: k['client_id']) for i, t in enumerate(harvest_task_names): t['id'] = i task_association = task_association_config(toggl_account, toggl_task_names, harvest_account, harvest_task_names) # organize toggl entries by dates worked delta = edate - sdate dates = [sdate + timedelta(days=i) for i in range(delta.days + 1)] combined_entries_dict = {} for date in dates: # collect entries from either platform on the given date from_toggl = [ x for x in toggl_entries if ((dateutil.parser.parse(x['start']).astimezone(toggl_tz) > toggl_tz.localize(date)) and ( dateutil.parser.parse(x['start']).astimezone(toggl_tz) <= toggl_tz.localize(date) + timedelta(days=1))) ] from_harvest = [ x['day_entry'] for x in harvest_entries if dateutil.parser.parse(x['day_entry']['spent_at']).astimezone( toggl_tz) == toggl_tz.localize(date) ] if from_toggl or from_harvest: combined_entries_dict[date] = { 'toggl': { 'raw': from_toggl, 'tasks': {} }, 'harvest': { 'raw': from_harvest, 'tasks': {} } } # organize raw entries into unique tasks, and total time for that day for platform in combined_entries_dict[date].keys(): for entry in combined_entries_dict[date][platform]['raw']: if platform == 'toggl': if entry['pid'] not in combined_entries_dict[date][ platform]['tasks'].keys(): combined_entries_dict[date][platform]['tasks'][ entry['pid']] = {} try: combined_entries_dict[date][platform]['tasks'][ entry['pid']][entry[ 'description']] += entry['dur'] / 3600000 except KeyError: combined_entries_dict[date][platform]['tasks'][ entry['pid']][entry[ 'description']] = entry['dur'] / 3600000 else: try: combined_entries_dict[date][platform]['tasks'][ entry['notes']] += entry['hours'] except KeyError: combined_entries_dict[date][platform]['tasks'][ entry['notes']] = entry['hours'] # add data to harvest add_to_harvest = [] for date, entry in combined_entries_dict.items(): if entry['toggl']['tasks'] and not entry['harvest']['tasks']: for pid in entry['toggl']['tasks'].keys(): for task in entry['toggl']['tasks'][pid].keys(): for hidpair in list( zip( task_association[pid][task] ['harvest_project_id'], task_association[pid] [task]['harvest_task_id'])): add_to_harvest.append({ 'project_id': hidpair[0], 'task_id': hidpair[1], 'spent_at': date.date().isoformat(), 'hours': round(entry['toggl']['tasks'][pid][task], 2), 'notes': task }) print("The following Toggl entries will be added to Harvest:") pp.pprint(add_to_harvest) if input("""Add the entries noted above to harvest? (y/n)""").lower() in ( 'y', 'yes'): for entry in add_to_harvest: pp.pprint( harvest_account.add_for_user(user_id=harvest_user_id, data=entry)) else: print('aborted') exit(1) print('done!') exit(0)
# Integration imports from credentials import TOGGL_TOKEN, AIRTABLE_API_KEY from toggl.TogglPy import Toggl from airtable import Airtable from airtable_integrations import * import gcal #Utilities import re from pprint import pprint from task_dicts import event_exclude import urllib toggl = Toggl() toggl.setAPIKey(TOGGL_TOKEN) offset = int(sys.argv[1]) #Generate today-string index = dt.today() + datetime.timedelta(offset) index_midnight = index.replace(hour=0, minute=1) index_formatted = index_midnight.isoformat() + 'Z' index_str = dt.strftime(index, '%Y-%m-%d') day_after_index = index + datetime.timedelta(1) day_after_index_midnight = day_after_index.replace(hour=0, minute=1) day_after_index_formatted = day_after_index_midnight.isoformat() + 'Z' #################################################### # Check if entries already exist in Toggl for date #
def getTogglClient(token: str) -> Toggl: toggl = Toggl() toggl.setAPIKey(token) return toggl
class TogglTimesheets: def __init__(self, api_key): # create a Toggl object and set our API key self.toggl = Toggl() self.toggl.setAPIKey(api_key) def _get_raw_timelogs(self, start_date=None, end_date=None): # Add filters params = {} if start_date: params["start_date"] = start_date if end_date: params["end_date"] = end_date # Make request and return return self.toggl.request(Endpoints.TIME_ENTRIES, parameters=params) def _get_raw_timelogs_last_n_days(self, n_days): last_n_days = datetime.utcnow() - timedelta(days=n_days) last_n_days_str = last_n_days.replace(microsecond=0, tzinfo=timezone.utc).isoformat() return self._get_raw_timelogs(start_date=last_n_days_str) @staticmethod def _parse_timelog(raw): """ { 'start': '2019-05-11T12:19:39+00:00', 'stop': '2019-05-11T12:40:27+00:00', 'description': 'Trying to deploy mongodb', 'tags': ['BB-1212'], 'duronly': False, } """ if raw.get('duronly', False): raise ("Error, timelog with duronly = true") tags = raw.get('tags', []) ticket = None dd = False ff = False for tag in tags: if ISSUE_REGEX.match(tag): ticket = tag elif tag == 'DD': dd = True elif tag == 'FF': ff = True if not ticket and 'description' in raw: possible = ISSUE_REGEX.findall(raw['description']) if possible: # TODO Dividing to multiple tickets ticket = possible[0] raw['description'] = raw['description'].strip(ticket).strip() if 'description' not in raw: # Shouldn't exist, but alas raw['description'] = 'NO DESCRIPTION' start = datetime.strptime(raw['start'], DATETIME_FORMAT) end = datetime.strptime(raw['stop'], DATETIME_FORMAT) return Timelog(ticket=ticket, date=start, time=end - start, description=raw['description'], ff_time=ff, dd_time=dd) def get_timelogs_last_n_days(self, n_days=1): raw_logs = self._get_raw_timelogs_last_n_days(n_days) result = { 'complete': list(), 'incomplete': list(), } for item in raw_logs: if 'stop' not in item: # Ignore currently running continue timelog = self._parse_timelog(item) if timelog.ticket: result['complete'].append(timelog) else: result['incomplete'].append(timelog) return result def get_timelogs(self, start_date, end_date): start_date_str = start_date.replace(microsecond=0, tzinfo=timezone.utc).isoformat() end_date_str = end_date.replace(microsecond=0, tzinfo=timezone.utc).isoformat() raw_logs = self._get_raw_timelogs(start_date=start_date_str, end_date=end_date_str) result = { 'complete': list(), 'incomplete': list(), } for item in raw_logs: if 'stop' not in item: # Ignore currently running continue timelog = self._parse_timelog(item) if timelog.ticket: result['complete'].append(timelog) else: result['incomplete'].append(timelog) return result
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)
def _get_toggl(user: User) -> Toggl: toggl = Toggl() toggl.setAPIKey(user.togglcredentials.api_key) return toggl
#get total amount of productivity in last 7 days from toggl.TogglPy import Toggl toggl = Toggl() toggl.setAPIKey('53a2d3c3856e1ef39982074936cd0c51') response = toggl.request("https://toggl.com/reports/api/v2/weekly") print(response)
class TogglTimesheets: def __init__(self, api_key): # create a Toggl object and set our API key self.toggl = Toggl() self.toggl.setAPIKey(api_key) def _get_raw_timelogs(self, start_date=None, end_date=None): # Add filters params = {} if start_date: params["start_date"] = start_date if end_date: params["start_date"] = end_date # Make request and return return self.toggl.request(Endpoints.TIME_ENTRIES, parameters=params) def _get_raw_timelogs_last_n_days(self, n_days): last_n_days = datetime.utcnow() - timedelta(days=n_days) last_n_days_str = last_n_days.replace(microsecond=0, tzinfo=timezone.utc).isoformat() return self._get_raw_timelogs(start_date=last_n_days_str) @staticmethod def _parse_timelog(raw): """ { 'start': '2019-05-11T12:19:39+00:00', 'stop': '2019-05-11T12:40:27+00:00', 'description': 'Trying to deploy mongodb', 'tags': ['BB-1212'], 'duronly': False, } """ if raw.get('duronly', False): print(raw) raise ("Error, timelog with duronly = true") tags = raw['tags'] ticket = None dd = False ff = False for tag in tags: if re.match(ISSUE_REGEX, tag): ticket = tag elif tag == 'DD': dd = True elif tag == 'FF': ff = True start = datetime.strptime(raw['start'], DATETIME_FORMAT) end = datetime.strptime(raw['stop'], DATETIME_FORMAT) return TIMELOG(ticket=ticket, date=start, time=end - start, description=raw['description'], ff_time=ff, dd_time=dd) def get_timelogs_last_n_days(self, n_days=1): raw_logs = self._get_raw_timelogs_last_n_days(n_days) result = { 'complete': list(), 'incomplete': list(), } for item in raw_logs: timelog = self._parse_timelog(item) if timelog.ticket: result['complete'].append(timelog) else: result['incomplete'].append(timelog) return result
def run(): # Connect with Google Calendar from google.auth import app_engine import googleapiclient.discovery # Second method using OAuth 2.0 from google.oauth2 import service_account project = "toggltocalendar" SCOPES = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events'] credentials = service_account.Credentials.from_service_account_file(os.environ['GOOGLE_APPLICATION_CREDENTIALS'], scopes=SCOPES) # Explicitly use App Engine credentials. These credentials are # only available when running on App Engine Standard. # credentials = app_engine.Credentials(scopes=SCOPES) # Explicitly pass the credentials to the client library. # storage_client = googleapiclient.discovery.build('storage', 'v1', credentials=credentials) # Make an authenticated API request # buckets = storage_client.buckets().list(project=project).execute() # print(buckets) # Connect with Google Calendar to read and create events service = googleapiclient.discovery.build('calendar', 'v3', credentials=credentials) calendars = service.calendarList().list().execute() print(calendars) # Create toggl object toggl = Toggl() toggl.setAPIKey('2e8cb0eb95b4d98c98564e4ba8ee26ee') duration = 1 # day(s) until = datetime.datetime.now() since = until - datetime.timedelta(days=duration) page = 1 more_results = True # Run while we have more results on the next page while (more_results): data = { 'workspace_id': 2705063, 'since': since.strftime("%Y-%m-%d"), 'until': until.strftime("%Y-%m-%d"), 'page': page } response = toggl.getDetailedReport(data) df = pd.DataFrame(response['data']) # For each new entry, create a new Calendar entry for i, row in df.iterrows: createNewCalendarEntry(i) page += 1 total_results = response['total_count'] more_results = ((page-1) * 50 < total_results)
class TogglDelegate(ZeiDelegate): def __init__(self, periph, config): self.config = config self.toggl = Toggl() self.toggl.setAPIKey(self.config["toggl"]["settings"]["token"]) self._populateProjects() self._populateMappings(self.config["mappings"]) super().__init__(periph) def handleNotification(self, cHandle, data): if cHandle == 38: # Side Change Notification side = struct.unpack("B", data)[0] self._trackProjectByMapping( self.mappings[side] if side in self.mappings else self.mappings[0] ) else: _log.info("Notification from hndl: %s - %r", cHandle, data) def _trackProjectByMapping(self, mapping: Mapping): self._trackProject( description=mapping.description, pid=mapping.id, tags=mapping.tags ) def _trackProject(self, description: str, pid: int, tags: list): current = self.toggl.currentRunningTimeEntry()["data"] if current is not None: if ( datetime.now(timezone.utc) - dateutil.parser.isoparse(current["start"]) ).total_seconds() < 20: # Delete entry if not older than 20s _log.info("Abort currently running entry") self.toggl.deleteTimeEntry(current["id"]) else: _log.info("Stopping currently running entry") self.toggl.stopTimeEntry(current["id"]) if pid not in self.projects: _log.info("Project not found, aborting") return _log.info( "Now tracking project %s: %s (%s)", self.projects[pid]["name"], description, ", ".join(tags if tags else []), ) notification.notify( title="Toggl", message=f"Now tracking project {self.projects[pid]['name']} {description} ({', '.join(tags) if tags else ''})", app_name="Toggl", app_icon=join(dirname(realpath(__file__)), "icons/toggl.png"), timeout=5, ) if pid == 0: return self.toggl.startTimeEntry(description, pid=pid, tags=tags) def _populateMappings(self, mappings: dict): self.mappings = {0: Mapping(0, 0)} for i in mappings: self.mappings[int(i)] = Mapping(int(i), int(mappings[i]["id"])) if "description" in mappings[i]: self.mappings[int(i)].description = mappings[i]["description"] if "tags" in mappings[i]: self.mappings[int(i)].tags = mappings[i]["tags"] def _populateProjects(self): self.projects = {} proj = self.toggl.getWorkspaceProjects( self.config["toggl"]["settings"]["workspace_id"] ) NoneProj = { "id": 0, "wid": int(self.config["toggl"]["settings"]["workspace_id"]), "name": "None", "billable": False, "is_private": True, "active": True, "template": False, "at": "2020-06-09T04:02:38+00:00", "created_at": "2019-12-09T16:36:28+00:00", "color": "9", "auto_estimates": False, "actual_hours": 0, "hex_color": "#990099", } self.projects[0] = NoneProj for i in proj: self.projects[i["id"]] = i
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()
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)
import os import urllib import datetime from toggl.TogglPy import Toggl from app import db from app.models import Workspace, Client, Member, Project, Task toggl = Toggl() TOGGL_API_KEY = os.environ.get('TOGGL_API_KEY') toggl.setAPIKey(TOGGL_API_KEY) DEFAULT_LONG_TASK = 3600000 # 1 hour WORKSPACE_ID = 3122278 USER_AGENT = 'api_test' REPORTS_URL = 'https://toggl.com/reports/api/v2/' WORKSPACES_URL = 'https://www.toggl.com/api/v8/workspaces/' PROJECTS_URL = 'https://www.toggl.com/api/v8/projects/' def toggl_api(duration=None): # Check WorkSpace workspace = Workspace.query.filter_by(id=WORKSPACE_ID).first() if workspace is None: workspace = Workspace(id=WORKSPACE_ID, name='Vitamin') db.session.add(workspace) db.session.commit()