Exemple #1
0
def sync_or_retrieve_time_entries(
        context_obj: dict,
        workspace: Workspace,
        start: datetime,
        stop: datetime,
        *,
        project: Optional[Project] = None) -> List[TimeEntry]:
    caching = retrieve_cache_from_context(context_obj)
    downloader = retrieve_downloader_from_context(context_obj)

    # Download any new time entries if remote sync is enabled.
    if context_obj['sync'] is True:
        current_time_entries = downloader.download_time_entries(start, stop)
        if current_time_entries:
            caching.update_time_entry_cache(current_time_entries)
    else:
        # Otherwise we just pull from the local cache.
        current_time_entries = caching.retrieve_time_entry_cache()

    # Neither downloading from remote nor pulling from local cache does any filtering, so let's do that.
    current_time_entries = TimeEntryFilter.filter_on_workspace(
        current_time_entries, workspace)

    # It also doesn't filter on start/stop, so let's do that too.
    current_time_entries = TimeEntryFilter.filter_on_date_range(
        current_time_entries, start, stop)

    # Time entries _can_ be filtered on project if it is provided.
    current_time_entries = TimeEntryFilter.filter_on_project(
        current_time_entries, project)

    return current_time_entries
Exemple #2
0
def timer_resume(context: click.Context):
    time_entries = context.obj['data']['time_entries']
    time_entries = sorted(time_entries,
                          key=lambda entry: entry.last_updated,
                          reverse=True)

    commands = retrieve_commands_from_context(context.obj)
    caching = retrieve_cache_from_context(context.obj)

    try:
        time_entry_builder = TimeEntryBuilder(time_entries[0])
        time_entry_builder.start_time(dt=datetime.now(get_localzone()))
        time_entry_builder.unset_stop_time()
        time_entry_builder.unset_duration()
        started_entry = commands.start_time_entry(time_entry_builder.build())
        if started_entry:
            caching.update_time_entry_cache([started_entry])
        click.echo(
            click.style('SUCCESS', fg='green') +
            f': Resumed time entry({started_entry.description}).')
    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to resume the latest timer. An exception'
            f' has been logged; check the logs for more information.')
Exemple #3
0
def tag_delete(context: click.Context, name: str, multiple: bool):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(click.style('ERROR', fg='red') +
                   ': a single workspace must be specified (default or otherwise'
                   ' ) when deleting a tag.')
        return

    current_tags = context.obj['data']['tags']

    if not current_tags:
        click.echo(click.style('WARNING') +
                   f': there are no tags in this workspace({workspace.name}).')
        return

    if name:
        current_tags = TagFilter.filter_on_name(
            current_tags, name
        )
        if not current_tags:
            click.echo(click.style('WARNING', fg='yellow') +
                       f': no tags exist with this name({name}'
                       f' in this workspace({workspace.name})')
            return

    if not multiple and len(current_tags) > 1:
        click.echo(click.style('ERROR', fg='red') +
                   ': multiple tags found matching the criteria, but --multiple '
                   'not specified.. please use the --multiple option or tighten '
                   'the search criteria.')
        return

    cache = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    # This raises if there is any issues, which means it doesn't get added
    # to the local cache.
    try:
        for tag in current_tags:
            commands.delete_tag(tag)

            # The new tag was removed from the remote Toggl servers, so now
            # we can remove it from our local cache as well.
            cache.remove_tag_from_cache(tag)
            click.echo(click.style('SUCCESS', fg='green') +
                       f': deleted tag({tag.name})!')

    except Exception as e:
        logging.getLogger(__name__).error(e)
        click.echo(click.style('ERROR', fg='red') +
                   f': failed to delete the tag(s). An exception has been logged;'
                   ' check the logs for more information.')
Exemple #4
0
def timer_start(context: click.Context, description: str, tags: str):
    time_entry_builder = TimeEntryBuilder()

    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when starting a timer.')
        return
    time_entry_builder.workspace_identifier(workspace.identifier)

    current_tags = context.obj['data']['tags']
    if tags:
        current_tags = TagFilter.filter_on_names(current_tags, tags.split(','))
        if not current_tags:
            click.echo(
                click.style('ERROR', fg='red') +
                f': no tags match the specified names.')
            return
        time_entry_builder.tags([tag.name for tag in current_tags])

    project = retrieve_project_from_context(context.obj)
    if project:
        time_entry_builder.project_identifier(project.identifier)

    if description:
        time_entry_builder.description(description)

    start_time = datetime.now(tz=get_localzone())
    time_entry_builder.start_time(dt=start_time)

    caching = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    try:
        entry = time_entry_builder.build()
        entry = commands.start_time_entry(entry)
        click.echo(
            click.style('SUCCESS', fg='green') +
            f': started new time entry({entry.description}) in'
            f' workspace({workspace.name}); start = {start_time.isoformat()}.')
        caching.update_time_entry_cache([entry])

    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to start the timer. An exception has been logged;'
            ' check the logs for more information.')
Exemple #5
0
def project_add(context: click.Context, name: str, color: str):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when adding a new project.')
        return

    # If there is a project with the same name existing already, do nothing.
    current_projects = context.obj['data']['projects']
    if current_projects:
        current_projects = ProjectFilter.filter_on_name(current_projects, name)
        if current_projects:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': project({name}) with this name already'
                f' exists in this workspace({workspace.name}).')
            return

    project = ProjectBuilder() \
        .name(name) \
        .workspace_identifier(workspace.identifier) \
        .color(Project.Color.from_string(color)) \
        .build()

    commands = retrieve_commands_from_context(context.obj)
    cache = retrieve_cache_from_context(context.obj)

    # This raises if there is any issues, which means it doesn't get added
    # to the local cache.
    try:
        added_project = commands.add_project(project)
        # The new project was added to the remote Toggl servers, so now
        # we can add it to our local cache as well.
        cache.update_project_cache([added_project])

        click.echo(
            click.style('SUCCESS', fg='green') +
            f': added new project({added_project.name}) to'
            f' workspace({workspace.name})!')

    except Exception as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to add new project({name}) to workspace({workspace.name}).'
            ' An exception has been logged; check the logs for more information.'
        )
Exemple #6
0
def sync_or_retrieve_workspaces(context_obj: dict) -> List[Workspace]:
    caching = retrieve_cache_from_context(context_obj)
    downloader = retrieve_downloader_from_context(context_obj)

    # Download any new workspaces added remotely (this is only possible via
    # the web interface anyway), if remote sync is enabled.
    if context_obj['sync'] is True:
        current_workspaces = downloader.download_workspaces()
        if current_workspaces:
            caching.update_workspace_cache(current_workspaces)
    else:
        # Otherwise we just pull from the local cache.
        current_workspaces = caching.retrieve_workspace_cache()

    return current_workspaces
Exemple #7
0
def sync_or_retrieve_tags(context_obj: dict, workspace: Workspace) -> List[Tag]:
    caching = retrieve_cache_from_context(context_obj)
    downloader = retrieve_downloader_from_context(context_obj)

    # Download any new tags if remote sync is enabled.
    if context_obj['sync'] is True:
        current_tags = downloader.download_tags(workspace)
        if current_tags:
            caching.update_tag_cache(current_tags)
    else:
        # Otherwise we just pull from the local cache.
        current_tags = caching.retrieve_tag_cache()

        if current_tags:
            # Retrieval from the DB doesn't filter on workspace, so we do that ourselves.
            current_tags = TagFilter.filter_on_workspace(current_tags, workspace)

    return current_tags
Exemple #8
0
def tag_add(context: click.Context, name: str):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace required to add a new tag.
        click.echo(click.style('ERROR', fg='red') +
                   ': a workspace is required when adding a new tag.')
        return

    current_tags = context.obj['data']['tags']
    if current_tags:
        current_tags = TagFilter.filter_on_name(
            current_tags, name)
        if current_tags:
            click.echo(click.style('WARNING', fg='yellow') +
                       f': tag({name}) with this name already'
                       f' exists in this workspace({workspace.name}).')
            return

    tag = TagBuilder()\
        .name(name)\
        .workspace_identifier(workspace.identifier)\
        .build()

    cache = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    # This raises if there is any issues, which means it doesn't get added
    # to the local cache.
    try:
        added_tag = commands.add_tag(tag)

        # The new tag was added to the remote Toggl servers, so now
        # we can add it to our local cache as well.
        cache.update_tag_cache([added_tag])
        click.echo(click.style('SUCCESS', fg='green') +
                   f': added new tag({added_tag.name}) to'
                   f' workspace({workspace.name})!')

    except Exception as e:
        logging.getLogger(__name__).error(e)
        click.echo(click.style('ERROR', fg='red') +
                   f': failed to add new tag({name}) to workspace({workspace.name}).'
                   ' An exception has been logged; check the logs for more information.')
Exemple #9
0
def timer_stop(context: click.Context):
    downloader = retrieve_downloader_from_context(context.obj)
    entry = downloader.get_current_time_entry()

    if not entry:
        click.echo('No entry is currently running.')
        return

    commands = retrieve_commands_from_context(context.obj)
    caching = retrieve_cache_from_context(context.obj)

    try:
        entry = commands.stop_time_entry(entry)
        caching.update_time_entry_cache([entry])
        click.echo(
            click.style('SUCCESS', fg='green') +
            f': stopped the current running timer({entry.description}).')

    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to stop the current running timer. An exception'
            f' has been logged; check the logs for more information.')
Exemple #10
0
def project_update(context: click.Context, old_name: str, old_color: str,
                   new_name: str, new_color: str, multiple: bool):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when updating a project.')
        return

    current_projects = context.obj['data']['projects']
    if not current_projects:
        click.echo(
            click.style('WARNING', fg='yellow') +
            f': no projects to update in this workspace({workspace.name}).')
        return

    if old_color:
        # See if the user specified a color to search on.
        current_projects = ProjectFilter.filter_on_color(
            current_projects, Project.Color.from_string(old_color))
        if not current_projects:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no projects exist with this color({old_color})'
                f' in this workspace({workspace.name}).')
            return

    if old_name:
        current_projects = ProjectFilter.filter_on_name(
            current_projects, old_name)
        if not current_projects:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no projects exist with this name({old_name}'
                f' in this workspace({workspace.name}).')
            return

    if not multiple and len(current_projects) > 1:
        click.echo(
            click.style('ERROR', fg='red') +
            ': multiple projects found matching the criteria, but --multiple '
            'not specified.. please used the --multiple option or tighten '
            'the search criteria.')
        return

    current_builds: List[ProjectBuilder] = []
    for project in current_projects:
        current_builds.append(ProjectBuilder(project))

    if new_name:
        for build in current_builds:
            assert isinstance(build, ProjectBuilder)
            build.name(new_name)
    if new_color:
        for build in current_builds:
            assert isinstance(build, ProjectBuilder)
            build.color(Project.Color.from_string(new_color))

    cache = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    try:
        built_projects = []
        for project_builder in current_builds:
            project = project_builder.build()
            project = commands.update_project(project)
            click.echo(
                click.style('SUCCESS', fg='green') +
                f': updated project({project.name}) in'
                f' workspace({workspace.name}).')
            built_projects.append(project)
        cache.update_project_cache(built_projects)

    except Exception as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to update the project(s). An exception has been logged;'
            ' check the logs for more information.')
Exemple #11
0
def project_delete(context: click.Context, name: str, color: str,
                   multiple: bool):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when deleting a project.')
        return

    current_projects = context.obj['data']['projects']
    if not current_projects:
        click.echo(
            click.style('WARNING', fg='yellow') +
            f': no projects exist in this workspace({workspace.name})!')
        return

    if color:
        # See if the user specified a color to search on.
        current_projects = ProjectFilter.filter_on_color(
            current_projects, Project.Color.from_string(color))
        if not current_projects:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no projects exist with this color({color})'
                f' in this workspace({workspace.name}).')
            return

    if name:
        current_projects = ProjectFilter.filter_on_name(current_projects, name)
        if not current_projects:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no projects exist with this name({name}'
                f' in this workspace({workspace.name}).')
            return

    if not multiple and len(current_projects) > 1:
        # If multiple matches are found but the user didn't specify the --multiple option,
        # prevent accidental deletion by requiring the user do it again but with the option
        # enabled.
        click.echo(
            click.style('ERROR', fg='red') +
            ': multiple projects found matching the criteria, but --multiple '
            'not specified.. please use the --multiple option or tighten '
            'the search criteria.')
        return

    commands = retrieve_commands_from_context(context.obj)
    cache = retrieve_cache_from_context(context.obj)

    try:
        if len(current_projects) > 1:
            commands.delete_projects(current_projects)
            for project in current_projects:
                cache.remove_project_from_cache(project)
                click.echo(
                    click.style('SUCCESS', fg='green') +
                    f': deleted project({project.name})!')
        else:
            commands.delete_project(current_projects[0])
            cache.remove_project_from_cache(current_projects[0])
            click.echo(
                click.style('SUCCESS', fg='green') +
                f': deleted project({current_projects[0].name})!')

    except Exception as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to delete the project(s). An exception has been logged;'
            ' check the logs for more information.')
Exemple #12
0
def timer_update(context: click.Context, old_description: str,
                 new_description: str, new_project: str, old_tags: str,
                 add_tags: str, remove_tags: str, new_start_time: datetime,
                 new_duration: int, new_stop_time: datetime, multiple: bool):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when updating a timer.')
        return

    if not old_description and not old_tags:
        click.echo(
            click.style('ERROR', fg='red') +
            f': either description or tags must be specified when updating a timer.'
        )
        return

    time_entries = context.obj['data']['time_entries']
    if not time_entries:
        click.echo(
            click.style('ERROR', fg='red') +
            f': no time entries to update in this workspace({workspace.name}).'
        )
        return

    if old_description:
        time_entries = TimeEntryFilter.filter_on_description(
            time_entries, old_description)
        if not time_entries:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no time entries exist with the specified description.')
            return

    current_tags = context.obj['data']['tags']
    if old_tags:
        current_tags = TagFilter.filter_on_names(current_tags,
                                                 old_tags.split(','))
        if not current_tags:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no tags exist with the specified names.')
            return

        time_entries = TimeEntryFilter.filter_on_any_tags(
            time_entries, current_tags)
        if not time_entries:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no time entries exist with the specified tags.')
            return
        # restore current_tags so we can filter it again if needed.
        current_tags = context.obj['data']['tags']

    if len(time_entries) > 1 and not multiple:
        click.echo(
            click.style('ERROR', fg='red') +
            ': multiple timers found matching the criteria, but --multiple '
            'not specified.. please used the --multiple option or tighten '
            'the search criteria.')
        return

    projects = context.obj['data']['projects']
    new_specified_project: Optional[Project] = None
    if new_project:
        projects = ProjectFilter.filter_on_name(projects, new_project)
        if not projects:
            click.echo(
                click.style('ERROR', fg='red') +
                f': no projects found with specified name.')
            return
        elif len(projects) > 1:
            click.echo(
                click.style('ERROR', fg='red') +
                f': multiple projects found with specified name. Update criteria '
                f' to match a single project.')
            return
        new_specified_project = projects[0]

    additional_tags: Optional[List[Tag]] = None
    if add_tags:
        additional_tags = TagFilter.filter_on_names(current_tags,
                                                    add_tags.split(','))
        if not additional_tags:
            click.echo(
                click.style('ERROR', fg='red') +
                f': no tags found with the specified names in --add-tags.')
            return
        # restore current_tags so we can filter it again if needed.
        current_tags = context.obj['data']['tags']

    removed_tags: Optional[List[Tag]] = None
    if remove_tags:
        removed_tags = TagFilter.filter_on_names(current_tags,
                                                 remove_tags.split(','))
        if not removed_tags:
            click.echo(
                click.style('ERROR', fg='red') +
                f': no tags found with the specified names in --remove-tags.')
            return

    entries: List[TimeEntry] = []
    for time_entry in time_entries:
        time_entry_builder = TimeEntryBuilder(time_entry)
        if new_description:
            time_entry_builder.description(new_description)
        if new_project:
            time_entry_builder.project_identifier(
                new_specified_project.identifier)
        if add_tags:
            time_entry_builder.add_tags([tag.name for tag in additional_tags])
        if remove_tags:
            time_entry_builder.remove_tags([tag.name for tag in removed_tags])
        if new_start_time:
            time_entry_builder.start_time(dt=new_start_time)
        if new_duration:
            time_entry_builder.duration(new_duration)
        if new_stop_time:
            time_entry_builder.stop_time(dt=new_stop_time)
        entries.append(time_entry_builder.build())

    caching = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    try:
        updated_entries: List[TimeEntry] = []
        for entry in entries:
            updated_entry = commands.update_completed_time_entry(entry)
            updated_entries.append(updated_entry)
            click.echo(
                click.style('SUCCESS', fg='green') +
                f': updated time entry({updated_entry.description}) in '
                f' workspace({workspace.name})!')
        caching.update_time_entry_cache(updated_entries)

    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to update the timer(s). An exception has been logged;'
            ' check the logs for more information.')
Exemple #13
0
def timer_delete(context: click.Context, description: str, multiple: bool,
                 tags: str):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when deleting a time entry.')
        return

    if not description and not tags:
        click.echo(
            click.style('ERROR', fg='red') +
            f': either description or tags must be specified when deleting a timer.'
        )
        return

    time_entries = context.obj['data']['time_entries']

    if description:
        time_entries = TimeEntryFilter.filter_on_description(
            time_entries, description)
        if not time_entries:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no time entries exist with the specified description.')
            return

    current_tags = context.obj['data']['tags']
    if tags:
        current_tags = TagFilter.filter_on_names(current_tags, tags.split(','))
        if not current_tags:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no tags exist with the specified names.')
            return

        time_entries = TimeEntryFilter.filter_on_any_tags(
            time_entries, current_tags)
        if not time_entries:
            click.echo(
                click.style('WARNING', fg='yellow') +
                f': no time entries exist with the specified tags.')
            return

    if len(time_entries) > 1 and not multiple:
        click.echo(
            click.style('ERROR', fg='red') +
            ': multiple time entries found matching the criteria, but --multiple '
            'not specified.. please use the --multiple option or tighten '
            'the search criteria.')
        return

    caching = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    try:
        for entry in time_entries:
            commands.delete_time_entry(entry)
            caching.remove_time_entry_from_cache(entry)
            click.echo(
                click.style('SUCCESS', fg='green') +
                f': deleted timer({entry.description})!')

    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to delete the timer(s). An exception has been logged;'
            ' check the logs for more information.')
Exemple #14
0
def timer_add(context: click.Context, description: str, start_time: datetime,
              stop_time: datetime, duration: int, tags: str):
    workspace = retrieve_workspace_from_context(context.obj)
    if not workspace:
        # Workspace filter was not strict enough or there is no default
        # workspace configured.
        click.echo(
            click.style('ERROR', fg='red') +
            ': a single workspace must be specified (default or otherwise'
            ' ) when adding a new timer.')
        return

    # Handling the error situations:
    if not stop_time and not duration:
        click.echo(
            click.style('ERROR', fg='red') +
            ': you must specify a stop time or duration when adding a new timer.'
        )
        return

    time_entries = context.obj['data']['time_entries']
    time_entries = TimeEntryFilter.filter_on_description(
        time_entries, description)

    if time_entries and description:
        click.echo(
            click.style('ERROR', fg='red') +
            ': a timer with this description already exists.')
        return

    time_entry_builder = TimeEntryBuilder()
    time_entry_builder.workspace_identifier(workspace.identifier)

    project = retrieve_project_from_context(context.obj)
    if project:
        time_entry_builder.project_identifier(project.identifier)

    if description:
        time_entry_builder.description(description)
    if start_time:
        time_entry_builder.start_time(dt=start_time)
    if stop_time:
        time_entry_builder.stop_time(dt=stop_time)
    if duration:
        time_entry_builder.duration(duration)
    elif stop_time:
        time_entry_builder.duration(
            int(stop_time.timestamp() - start_time.timestamp()))
    if tags:
        time_entry_builder.tags(tags.split(','))

    caching = retrieve_cache_from_context(context.obj)
    commands = retrieve_commands_from_context(context.obj)

    try:
        time_entry = time_entry_builder.build()
        time_entry = commands.add_completed_time_entry(time_entry)
        caching.update_time_entry_cache([time_entry])

        click.echo(
            click.style('SUCCESS', fg='green') + f': added new time entry to'
            f' workspace({workspace.name})!')

    except HTTPError as e:
        logging.getLogger(__name__).error(e)
        click.echo(
            click.style('ERROR', fg='red') +
            f': failed to add new time entry to workspace({workspace.name}).'
            ' An exception has been logged; check the logs for more information.'
        )