def __init__(self, *args, **kw): super(TogglWorkflow, self).__init__(*args, **kw) self.cache = JsonFile(os.path.join(self.cache_dir, 'cache.json'), ignore_errors=True) self.config.header = CONFIG_HEADER.strip() if 'use_notifier' not in self.config: self.config['use_notifier'] = False if 'api_key' not in self.config: self.show_message( 'First things first...', 'Before you can use this workflow, you need ' 'to set your Toggl API key. You can find your ' 'key on the My Profile page at Toggl.com') answer, key = self.get_from_user('Set an API key', 'Toggl API key') if answer == 'Ok': self.config['api_key'] = key self.show_message('Good to go', 'Your key has been set!') toggl.api_key = self.config['api_key'] if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to ' 'set api key to "{0}"'.format( self.config['api_key']))
def __init__(self, *args, **kw): super(TogglWorkflow, self).__init__(*args, **kw) self.cache = JsonFile(os.path.join(self.cache_dir, 'cache.json'), ignore_errors=True) self.config.header = CONFIG_HEADER.strip() if 'use_notifier' not in self.config: self.config['use_notifier'] = False if 'api_key' not in self.config: self.show_message('First things first...', 'Before you can use this workflow, you need ' 'to set your Toggl API key. You can find your ' 'key on the My Profile page at Toggl.com') answer, key = self.get_from_user('Set an API key', 'Toggl API key') if answer == 'Ok': self.config['api_key'] = key self.show_message('Good to go', 'Your key has been set!') toggl.api_key = self.config['api_key'] if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to ' 'set api key to "{0}"'.format( self.config['api_key']))
def _load_settings(self): '''Get an the location and units to use''' version = self.config.get('version') if version is not None and version < SETTINGS_VERSION: self._migrate_settings() self.config['version'] = SETTINGS_VERSION self.config['units'] = self.config.get('units', DEFAULT_UNITS); self.config['icons'] = self.config.get('icons', DEFAULT_ICONS); self.config['time_format'] = self.config.get('time_format', DEFAULT_TIME_FMT); self.config['days'] = self.config.get('days', 3); import os.path old_config_file = os.path.join(self.data_dir, 'settings.json') if (os.path.exists(old_config_file) and not self.config.get('migrated')): old_config = JsonFile(old_config_file) for key, value in old_config.items(): self.config[key] = value self.config['migrated'] = True
def _load_settings(self): '''Get an the location and units to use''' version = self.config.get('version') if version is not None and version < SETTINGS_VERSION: self._migrate_settings() self.config['version'] = SETTINGS_VERSION self.config['units'] = self.config.get('units', DEFAULT_UNITS) self.config['icons'] = self.config.get('icons', DEFAULT_ICONS) self.config['time_format'] = self.config.get('time_format', DEFAULT_TIME_FMT) self.config['days'] = self.config.get('days', 3) self.config['show_localtime'] = self.config.get('show_localtime', True) import os.path old_config_file = os.path.join(self.data_dir, 'settings.json') if (os.path.exists(old_config_file) and not self.config.get('migrated')): old_config = JsonFile(old_config_file) for key, value in old_config.items(): self.config[key] = value self.config['migrated'] = True
class TogglWorkflow(Workflow): def __init__(self, *args, **kw): super(TogglWorkflow, self).__init__(*args, **kw) self.cache = JsonFile(os.path.join(self.cache_dir, 'cache.json'), ignore_errors=True) self.config.header = CONFIG_HEADER.strip() if 'use_notifier' not in self.config: self.config['use_notifier'] = False if 'api_key' not in self.config: self.show_message( 'First things first...', 'Before you can use this workflow, you need ' 'to set your Toggl API key. You can find your ' 'key on the My Profile page at Toggl.com') answer, key = self.get_from_user('Set an API key', 'Toggl API key') if answer == 'Ok': self.config['api_key'] = key self.show_message('Good to go', 'Your key has been set!') toggl.api_key = self.config['api_key'] if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to ' 'set api key to "{0}"'.format( self.config['api_key'])) def tell_query(self, query, start=None, end=None): '''List entries that match a query. Note that an end time without a start time will be ignored.''' LOG.info('tell_query("{0}", start={1}, end={2})'.format( query, start, end)) if not start: end = None needs_refresh = False query = query.strip() if self.cache.get('disable_cache', False): LOG.debug('cache is disabled') needs_refresh = True elif self.cache.get('time') and self.cache.get('time_entries'): last_load_time = self.cache.get('time') LOG.debug('last load was %s', last_load_time) import time now = int(time.time()) if now - last_load_time > CACHE_LIFETIME: LOG.debug('automatic refresh') needs_refresh = True else: LOG.debug('cache is missing timestamp or data') needs_refresh = True if needs_refresh: LOG.debug('refreshing cache') try: all_entries = toggl.TimeEntry.all() except Exception: LOG.exception('Error getting time entries') raise Exception('Problem talking to toggl.com') import time self.cache['time'] = int(time.time()) self.cache['time_entries'] = serialize_entries(all_entries) else: LOG.debug('using cached data') all_entries = deserialize_entries(self.cache['time_entries']) LOG.debug('%d entries', len(all_entries)) if start: LOG.debug('filtering on start time %s', start) if end: LOG.debug('filtering on end time %s', end) all_entries = [ e for e in all_entries if e.start_time < end and e.stop_time > start ] else: all_entries = [e for e in all_entries if e.stop_time > start] LOG.debug('filtered to %d entries', len(all_entries)) efforts = {} # group entries with the same description into efforts (so as not to be # confused with Toggl tasks for entry in all_entries: if entry.description not in efforts: efforts[entry.description] = Effort(entry.description, start, end) efforts[entry.description].add(entry) efforts = efforts.values() efforts = sorted(efforts, reverse=True, key=lambda e: e.newest_entry.start_time) items = [] if start: if len(efforts) > 0: seconds = sum(e.seconds for e in efforts) LOG.debug('total seconds: %s', seconds) total_time = to_hours_str(seconds) if end: item = Item('{0} hours on {1}'.format( total_time, start.date().strftime(DATE_FORMAT)), subtitle=Item.LINE) else: item = Item('{0} hours from {1}'.format( total_time, start.date().strftime(DATE_FORMAT)), subtitle=Item.LINE) else: item = Item('Nothing to report') items.append(item) show_suffix = start or end for effort in efforts: item = Item(effort.description, valid=True) now = LOCALTZ.localize(datetime.datetime.now()) newest_entry = effort.newest_entry if newest_entry.is_running: item.icon = 'running.png' started = newest_entry.start_time delta = to_approximate_time(now - started) seconds = effort.seconds total = '' if seconds > 0: hours = to_hours_str(seconds, show_suffix=show_suffix) total = ' ({0} hours total)'.format(hours) item.subtitle = 'Running for {0}{1}'.format(delta, total) item.arg = 'stop|{0}|{1}'.format(newest_entry.id, effort.description) else: seconds = effort.seconds hours = to_hours_str(datetime.timedelta(seconds=seconds), show_suffix=show_suffix) if start: item.subtitle = ('{0} hours'.format(hours)) else: stop = newest_entry.stop_time if stop: delta = to_approximate_time(now - stop, ago=True) else: delta = 'recently' oldest = effort.oldest_entry since = oldest.start_time since = since.strftime('%m/%d') item.subtitle = ('{0} hours since {1}, ' 'stopped {2}'.format(hours, since, delta)) item.arg = 'continue|{0}|{1}'.format(newest_entry.id, effort.description) items.append(item) if len(query.strip()) > 1: # there's a filter test = query[1:].strip() items = self.fuzzy_match_list(test, items, key=lambda t: t.title) if len(items) == 0: items.append(Item("Nothing found")) return items def tell_since(self, query): '''Return info about entries since a time A time may be: - a date - one of {'today', 'yesterday', 'this week'} ''' query = query.strip() if not query: return [ Item('Enter a start time', subtitle='This can be a time, ' 'date, datetime, "yesterday", "tuesday", ...') ] return self.tell_query('', start=get_start(query)) def tell_on(self, query): '''Return info about entries over a span A span may be: - a single start date, which denotes a span from that date to now - one of {'today', 'yesterday', 'this week'} - a week day name ''' query = query.strip() if not query: return [ Item('Enter a date', subtitle='9/8, yesterday, monday, ' '...') ] return self.tell_query('', start=get_start(query), end=get_end(query)) def tell_start(self, query): LOG.info('adding a new time entry...') items = [] desc = query.strip() if desc: items.append( Item('Creating timer "{0}"...'.format(desc), arg='start|' + desc, valid=True)) else: items.append(Item('Waiting for description...')) return items def tell_help(self, query): items = [] items.append( Item("Use '/' to list existing timers", subtitle='Type some text to filter the results')) items.append( Item("Use '//' to force a cache refresh", subtitle='Data from Toggl is normally cached for ' '{0} seconds'.format(CACHE_LIFETIME))) items.append( Item("Use '<' to list timers started since a time", subtitle='9/2, 9/2/13, 2013-9-2T22:00-04:00, ...')) items.append( Item("Use '@' to list time spent on a particular date", subtitle='9/2, 9/2/13, 2013-9-2T22:00-04:00, ...')) items.append( Item("Use '+' to start a new timer", subtitle="Type a description after the '+'")) items.append( Item("Use '>' to access other commands", subtitle='Enable menubar icon, go to toggl.com, ' '...')) items.append(Item("Select an existing timer to toggle it")) return items def tell_commands(self, query): LOG.info('telling cmd with "{0}"'.format(query)) items = [] items.append( Item('Open toggl.com', arg='open|https://new.toggl.com/app', subtitle='Open a browser tab for toggl.com', valid=True)) items.append( Item('Open the workflow config file', arg='open|' + self.config_file, subtitle='Change workflow options here, like the ' 'debug log level', valid=True)) items.append( Item('Open the debug log', arg='open|' + self.log_file, subtitle='Open a browser tab for toggl.com', valid=True)) if self.config['use_notifier']: items.append( Item('Disable the menubar notifier', subtitle='Exit and disable the menubar notifier', arg='disable_notifier', valid=True)) else: items.append( Item('Enable the menubar notifier', subtitle='Start and enable the menubar notifier', arg='enable_notifier', valid=True)) items.append( Item('Clear the cache', subtitle='Force a cache refresh on the next query', arg='force_refresh', valid=True)) if 'api_key' in self.config: items.append( Item('Forget your API key', subtitle='Forget your stored API key, allowing ' 'you to change it', arg='clear_key', valid=True)) if len(query.strip()) > 1: # there's a filter items = self.fuzzy_match_list(query.strip(), items, key=lambda t: t.title) if len(items) == 0: items.append(Item("Invalid command")) return items def do_action(self, query): LOG.info('doing action with %s', query) cmd, sep, arg = query.partition('|') if cmd == 'start': entry = toggl.TimeEntry.start(arg) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to set ' 'active timer to "{0}|{1}"'.format( entry.id, arg)) self.puts('Started {0}'.format(arg)) elif cmd == 'continue': tid, sep, desc = arg.partition('|') entry = toggl.TimeEntry.start(desc) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to set ' 'active timer to "{0}|{1}"'.format( entry.id, desc)) self.puts('Continued {0}'.format(desc)) elif cmd == 'stop': tid, sep, desc = arg.partition('|') toggl.TimeEntry.stop(tid) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to be ' 'stopped') self.puts('Stopped {0}'.format(desc)) elif cmd == 'enable_notifier': self.config['use_notifier'] = True self.run_script('tell application "TogglNotifier" to activate') self.run_script('tell application "TogglNotifier" to set api key ' 'to "{0}"'.format(toggl.api_key)) self.puts('Notifier enabled') elif cmd == 'disable_notifier': self.config['use_notifier'] = False self.run_script('tell application "TogglNotifier" to quit') self.puts('Notifier disabled') elif cmd == 'clear_key': del self.config['api_key'] self.run_script('tell application "TogglNotifier" to quit') self.puts('Cleared API key') elif cmd == 'force_refresh': self.cache['time_entries'] = None elif cmd == 'open': from subprocess import call call(['open', arg]) else: self.puts('Unknown command "{0}"'.format(cmd)) def schedule_refresh(self): '''Force a refresh next time Toggl is queried''' self.cache['time'] = 0
def cache(self): if not self._cache: self._cache = JsonFile(self.cache_file) return self._cache
class TogglWorkflow(Workflow): def __init__(self, *args, **kw): super(TogglWorkflow, self).__init__(*args, **kw) self.cache = JsonFile(os.path.join(self.cache_dir, 'cache.json'), ignore_errors=True) self.config.header = CONFIG_HEADER.strip() if 'use_notifier' not in self.config: self.config['use_notifier'] = False if 'api_key' not in self.config: self.show_message('First things first...', 'Before you can use this workflow, you need ' 'to set your Toggl API key. You can find your ' 'key on the My Profile page at Toggl.com') answer, key = self.get_from_user('Set an API key', 'Toggl API key') if answer == 'Ok': self.config['api_key'] = key self.show_message('Good to go', 'Your key has been set!') toggl.api_key = self.config['api_key'] if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to ' 'set api key to "{0}"'.format( self.config['api_key'])) def tell_query(self, query, start=None, end=None): '''List entries that match a query. Note that an end time without a start time will be ignored.''' LOG.info('tell_query("{0}", start={1}, end={2})'.format( query, start, end)) if not start: end = None needs_refresh = False query = query.strip() if self.cache.get('disable_cache', False): LOG.debug('cache is disabled') needs_refresh = True elif self.cache.get('time') and self.cache.get('time_entries'): last_load_time = self.cache.get('time') LOG.debug('last load was %s', last_load_time) import time now = int(time.time()) if now - last_load_time > CACHE_LIFETIME: LOG.debug('automatic refresh') needs_refresh = True else: LOG.debug('cache is missing timestamp or data') needs_refresh = True if needs_refresh: LOG.debug('refreshing cache') try: all_entries = toggl.TimeEntry.all() except Exception: LOG.exception('Error getting time entries') raise Exception('Problem talking to toggl.com') import time self.cache['time'] = int(time.time()) self.cache['time_entries'] = serialize_entries(all_entries) else: LOG.debug('using cached data') all_entries = deserialize_entries(self.cache['time_entries']) LOG.debug('%d entries', len(all_entries)) if start: LOG.debug('filtering on start time %s', start) if end: LOG.debug('filtering on end time %s', end) all_entries = [e for e in all_entries if e.start_time < end and e.stop_time > start] else: all_entries = [e for e in all_entries if e.stop_time > start] LOG.debug('filtered to %d entries', len(all_entries)) efforts = {} # group entries with the same description into efforts (so as not to be # confused with Toggl tasks for entry in all_entries: if entry.description not in efforts: efforts[entry.description] = Effort(entry.description, start, end) efforts[entry.description].add(entry) efforts = efforts.values() efforts = sorted(efforts, reverse=True, key=lambda e: e.newest_entry.start_time) items = [] if start: if len(efforts) > 0: seconds = sum(e.seconds for e in efforts) LOG.debug('total seconds: %s', seconds) total_time = to_hours_str(seconds) if end: item = Item('{0} hours on {1}'.format( total_time, start.date().strftime(DATE_FORMAT)), subtitle=Item.LINE) else: item = Item('{0} hours from {1}'.format( total_time, start.date().strftime(DATE_FORMAT)), subtitle=Item.LINE) else: item = Item('Nothing to report') items.append(item) show_suffix = start or end for effort in efforts: item = Item(effort.description, valid=True) now = LOCALTZ.localize(datetime.datetime.now()) newest_entry = effort.newest_entry if newest_entry.is_running: item.icon = 'running.png' started = newest_entry.start_time delta = to_approximate_time(now - started) seconds = effort.seconds total = '' if seconds > 0: hours = to_hours_str(seconds, show_suffix=show_suffix) total = ' ({0} hours total)'.format(hours) item.subtitle = 'Running for {0}{1}'.format(delta, total) item.arg = 'stop|{0}|{1}'.format(newest_entry.id, effort.description) else: seconds = effort.seconds hours = to_hours_str(datetime.timedelta(seconds=seconds), show_suffix=show_suffix) if start: item.subtitle = ('{0} hours'.format(hours)) else: stop = newest_entry.stop_time if stop: delta = to_approximate_time(now - stop, ago=True) else: delta = 'recently' oldest = effort.oldest_entry since = oldest.start_time since = since.strftime('%m/%d') item.subtitle = ('{0} hours since {1}, ' 'stopped {2}'.format(hours, since, delta)) item.arg = 'continue|{0}|{1}'.format(newest_entry.id, effort.description) items.append(item) if len(query.strip()) > 1: # there's a filter test = query[1:].strip() items = self.fuzzy_match_list(test, items, key=lambda t: t.title) if len(items) == 0: items.append(Item("Nothing found")) return items def tell_since(self, query): '''Return info about entries since a time A time may be: - a date - one of {'today', 'yesterday', 'this week'} ''' query = query.strip() if not query: return [Item('Enter a start time', subtitle='This can be a time, ' 'date, datetime, "yesterday", "tuesday", ...')] return self.tell_query('', start=get_start(query)) def tell_on(self, query): '''Return info about entries over a span A span may be: - a single start date, which denotes a span from that date to now - one of {'today', 'yesterday', 'this week'} - a week day name ''' query = query.strip() if not query: return [Item('Enter a date', subtitle='9/8, yesterday, monday, ' '...')] return self.tell_query('', start=get_start(query), end=get_end(query)) def tell_start(self, query): LOG.info('adding a new time entry...') items = [] desc = query.strip() if desc: items.append(Item('Creating timer "{0}"...'.format(desc), arg='start|' + desc, valid=True)) else: items.append(Item('Waiting for description...')) return items def tell_help(self, query): items = [] items.append(Item("Use '/' to list existing timers", subtitle='Type some text to filter the results')) items.append(Item("Use '//' to force a cache refresh", subtitle='Data from Toggl is normally cached for ' '{0} seconds'.format(CACHE_LIFETIME))) items.append(Item("Use '<' to list timers started since a time", subtitle='9/2, 9/2/13, 2013-9-2T22:00-04:00, ...')) items.append(Item("Use '@' to list time spent on a particular date", subtitle='9/2, 9/2/13, 2013-9-2T22:00-04:00, ...')) items.append(Item("Use '+' to start a new timer", subtitle="Type a description after the '+'")) items.append(Item("Use '>' to access other commands", subtitle='Enable menubar icon, go to toggl.com, ' '...')) items.append(Item("Select an existing timer to toggle it")) return items def tell_commands(self, query): LOG.info('telling cmd with "{0}"'.format(query)) items = [] items.append(Item('Open toggl.com', arg='open|https://new.toggl.com/app', subtitle='Open a browser tab for toggl.com', valid=True)) items.append(Item('Open the workflow config file', arg='open|' + self.config_file, subtitle='Change workflow options here, like the ' 'debug log level', valid=True)) items.append(Item('Open the debug log', arg='open|' + self.log_file, subtitle='Open a browser tab for toggl.com', valid=True)) if self.config['use_notifier']: items.append(Item('Disable the menubar notifier', subtitle='Exit and disable the menubar notifier', arg='disable_notifier', valid=True)) else: items.append(Item('Enable the menubar notifier', subtitle='Start and enable the menubar notifier', arg='enable_notifier', valid=True)) items.append(Item('Clear the cache', subtitle='Force a cache refresh on the next query', arg='force_refresh', valid=True)) if 'api_key' in self.config: items.append(Item('Forget your API key', subtitle='Forget your stored API key, allowing ' 'you to change it', arg='clear_key', valid=True)) if len(query.strip()) > 1: # there's a filter items = self.fuzzy_match_list(query.strip(), items, key=lambda t: t.title) if len(items) == 0: items.append(Item("Invalid command")) return items def do_action(self, query): LOG.info('doing action with %s', query) cmd, sep, arg = query.partition('|') if cmd == 'start': entry = toggl.TimeEntry.start(arg) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to set ' 'active timer to "{0}|{1}"'.format(entry.id, arg)) self.puts('Started {0}'.format(arg)) elif cmd == 'continue': tid, sep, desc = arg.partition('|') entry = toggl.TimeEntry.start(desc) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to set ' 'active timer to "{0}|{1}"'.format(entry.id, desc)) self.puts('Continued {0}'.format(desc)) elif cmd == 'stop': tid, sep, desc = arg.partition('|') toggl.TimeEntry.stop(tid) self.schedule_refresh() if self.config['use_notifier']: self.run_script('tell application "TogglNotifier" to be ' 'stopped') self.puts('Stopped {0}'.format(desc)) elif cmd == 'enable_notifier': self.config['use_notifier'] = True self.run_script('tell application "TogglNotifier" to activate') self.run_script('tell application "TogglNotifier" to set api key ' 'to "{0}"'.format(toggl.api_key)) self.puts('Notifier enabled') elif cmd == 'disable_notifier': self.config['use_notifier'] = False self.run_script('tell application "TogglNotifier" to quit') self.puts('Notifier disabled') elif cmd == 'clear_key': del self.config['api_key'] self.run_script('tell application "TogglNotifier" to quit') self.puts('Cleared API key') elif cmd == 'force_refresh': self.cache['time_entries'] = None elif cmd == 'open': from subprocess import call call(['open', arg]) else: self.puts('Unknown command "{0}"'.format(cmd)) def schedule_refresh(self): '''Force a refresh next time Toggl is queried''' self.cache['time'] = 0