Giter VIP home page Giter VIP logo

toggl-cli's Introduction

Toggl CLI

PyPI version PyPI - Python Version PyPI - Downloads codecov Build Status

Command line tool and set of Python wrapper classes for interacting with toggl's API

Install

Easiest way to install this package is through PyPi:

$ pip install togglCli

Usage

For full overview of Toggl CLI capabilities please see full documentation.

CLI tool

With first run of the command you will be asked several questions to bootstrap default config file (only UNIX-like system are supported; for Window's users there is created dummy config file, which you have to setup manually).

To get overview of all commands and options please use --help option. Check out also help pages of the subcommands!

Several examples of commands:

# Starts tracking new time entry
$ toggl start

# Displays/enable modifications of currently running time entry
$ toggl now

# Lists all projects
$ toggl projects ls

API wrappers

Toggl CLI comes with set of Python's class wrappers which follow similar pattern like Django ORM.

The wrappers depends on config object which if not provided, the default config file (eq. ~/.togglrc) is used.

Toggl CLI uses pendulum for datetime management, but it is compatible with Python's native datetime, so you can use that if you want to.

from toggl import api, utils
import pendulum

new_entry = api.TimeEntry(description='Some new time entry', start=pendulum.now() - pendulum.duration(minutes=15), stop=pendulum.now())
new_entry.save()

list_of_all_entries = api.TimeEntry.objects.all()

current_time_entry = api.TimeEntry.objects.current()

# Custom config from existing file
config = utils.Config.factory('./some.config')

# Custom config without relying on any existing config file 
config = utils.Config.factory(None)  # Without None it will load the default config file
config.api_token = 'your token'
config.timezone = 'utc'  # Custom timezone

project = api.Project.object.get(123, config=config)
project.name = 'Some new name'
project.save()

Contributing

Feel free to dive in, contributions are welcomed! Open an issue or submit PRs.

For PRs please see contribution guideline.

License

MIT © Adam Uhlir & D. Robert Adams

toggl-cli's People

Contributors

auhau avatar beauraines avatar bryant1410 avatar ddkasa avatar dependabot-preview[bot] avatar dependabot[bot] avatar devkev avatar dkvc avatar drobertadams avatar federicovaga avatar figarocorso avatar geetotes avatar github-actions[bot] avatar hugovk avatar jan-matejka avatar martinm76 avatar matescb avatar mikepurvis avatar moredread avatar mthowe avatar olen avatar pyup-bot avatar reekenx avatar shpoont avatar sjvrijn avatar staff0rd avatar stanczakdominik avatar tbrodbeck avatar theletterjeff avatar toddmazierski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

toggl-cli's Issues

Multiple Projects with same Tag

I have multiple projects with the same Tag Name
@monthly Marketing Work - Client A
@monthly Marketing Work - Client B

But anytime I try to run
toggle start "Fix Site" @monthly

It chooses Client B, and I'm not sure how to switch it to Client A. I tried "@monthly Marketing Work - Client A", but that didn't work either.

Two date formats in use

After the inclusion of the ical feature in #43, the cli looks as though it supports two different datetime formats per --help. DATETIME, the format existing prior to ical, which is I believe local time and is converted to UTC+TZ via the DateAndTime object, and the ical specified starttime/endtime of YYYY-MM-DDThh:mm:ss+TZ:00.

For consistency there should probably be only one DATETIME format. I'm not sure why ical needs the full TZ specifier (and I don't know how ical is supposed to work), perhaps @martinm76 can chime in.

add: command not working

Hi,

I am testing out the cli and when I try to use the add command I just see the help menu printed

Example command:

toggl add "Code reviews, and week prep" :Company @Project

Whereas when I start a new task to be logged:

toggl start "Code reviews, and week prep" :Company @Project

It works.

Seems like the add command line argument is not being picked up. Any ideas?

Licensing - Copyright?

You chose a copyright license for this tool. So that would mean that I cannot fork unless you give permission. And if I happen to contribute, my PR would also get your property, right? Did you consider swithing to a more liberal license? Or do you just not enforce your copyrights (there are already 14 forks and I could not find any issues asking for permission)?

zsh/bash completion?

In source I observe some signs that tool had automated zsh/bash completion some time ago?
https://github.com/click-contrib/click-completion

So far I am using my self-written "dirty one" which tab completes almost all routine tasks, but if tool has some built-in completion generator - can you share way to activate it ?

https://github.com/Voronenko/dotfiles/blob/master/completions/_toggl.zsh

#compdef toggl

__toggl-workspaces() {
    toggl workspaces
}

__toggl-projects() {
    toggl projects | awk '{ print $2 }'
}

__toggl-now() {
    echo "''d1:20''"
    echo "'`date '+%Y-%m-%d %H:%M:%S'`'"
}

__toggl-duration() {
    echo "''d1:20''"
}

__toggl-recent-entries() {
    toggl ls | grep @ | awk -F'@' '{gsub(/^[ \t\r\n]+/, "", $1);gsub(/[ \t\r\n]+$/, "", $1);print "\""$1"\""}' | sort | uniq
}


_toggl-commands() {
  local -a commands

  commands=(
    'add: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] START_DATETIME (''d''DURATION | END_DATETIME) creates a completed time entry, DURATION = [[Hours:]Minutes:]Seconds'
    'add: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] ''d''DURATION creates a completed time entry, with start time DURATION ago, DURATION = [[Hours:]Minutes:]Seconds'
    'clients:lists all clients'
    'continue: [from DATETIME | ''d''DURATION] restarts the last entry, DURATION = [[Hours:]Minutes:]Seconds'
    'continue: DESCR [from DATETIME | ''d''DURATION] restarts the last entry matching DESCR'
    'ls:list recent time entries'
    'now:print what you''re working on now'
    'workspaces:lists all workspaces'
    'projects: [:WORKSPACE] lists all projects'
    'rm:delete a time entry by id'
    'start: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] [''d''DURATION | DATETIME] starts a new entry , DURATION = [[Hours:]Minutes:]Seconds'
    'stop:stops the current entry'
    'www:visits toggl.com'
  )

  _arguments -s : $nul_args && ret=0
  _describe -t commands 'toggl command' commands && ret=0
}


_toggl() {
  local -a nul_args
  nul_args=(
    '(-h --help)'{-h,--help}'[show help message and exit.]'
    '(-q --quiet)'{-q,--quiet}'[don''t print anything]'
    '(-v --verbose)'{-v,--verbose}'[print additional info]'
    '(-d --debug)'{-d,--debug}'[print debugging output]'
  )

  local curcontext=$curcontext ret=1

  if ((CURRENT == 2)); then
    _toggl-commands
  else
    shift words
    (( CURRENT -- ))
    curcontext="${curcontext%:*:*}:toggl-$words[1]:"
    case $words[1] in
    add)
      _arguments "2: :($(__toggl-workspaces))" \
                 "3: :($(__toggl-projects))" \
                 "4: :($(__toggl-now))" \
                 "5: :($(__toggl-duration))"
      ;;
    start)
      _arguments "2: :($(__toggl-workspaces))" \
                 "3: :($(__toggl-projects))" \
                 "4: :($(__toggl-now))"
      ;;
    stop)
      _arguments "1: :($(__toggl-now))"
      ;;
    projects)
      _arguments "1: :($(__toggl-workspaces))"
      ;;
    continue)
      _arguments "1: :($(__toggl-recent-entries))"
      ;;


    esac  fi
}

_toggl "$@"

# Local Variables:
# mode: Shell-Script
# sh-indentation: 2
# indent-tabs-mode: nil
# sh-basic-offset: 2
# End:
# vim: ft=zsh sw=2 ts=2 et

KeyError when trying to run toggl.py

When I try to run toggle.py, I get a KeyError

Traceback (most recent call last):
  File "toggl.py", line 1175, in <module>
    CLI().act()
  File "toggl.py", line 979, in act
    Logger.info(TimeEntryList())
  File "toggl.py", line 271, in info
    print("%s%s" % (msg, end)),
  File "toggl.py", line 832, in __str__
    s += str(entry) + "\n"
  File "toggl.py", line 712, in __str__
    s = "%s%s%s%s" % (is_running, self.data['description'], project_name, 
KeyError: 'description'

UnboundLocalError: local variable 'r' referenced before assignment

I think this is because my .togglrc file is old (which I am about to fix), but nevertheless this is a simple bug that should be fixed:

$ toggl
Sent: None
No option 'prefer_token' in section: 'options'
Traceback (most recent call last):
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 1265, in <module>
    CLI().act()
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 1039, in act
    Logger.info(TimeEntryList())
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 82, in __call__
    cls.instance = super(Singleton, cls).__call__(*args, **kw)
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 791, in __init__
    self.reload()
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 849, in reload
    entries = json.loads( toggl(url, 'get') )
  File "/home/kev/g/devkev/toggl-cli/toggl.py", line 319, in toggl
    print(r.text)
UnboundLocalError: local variable 'r' referenced before assignment

Problem parsing european letters such as åäö

I'm from Sweden. In Sweden we use weird letters like åäö. Så when clients are called "Lantmännen" with projects called "Höstkampanj" you run into problems with toggl-cli. This happens when I try to access my company's toggl:

toggl clients
Traceback (most recent call last):
File "toggl.py", line 1181, in
CLI().act()
File "toggl.py", line 989, in act
print(ClientList())
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 281: ordinal not in range(128)

I'm not very familiar with python, but this seems to do the trick (line 493):

return s.rstrip().encode('utf-8')

Since I'm no pro at python, nor git, I think it's smarter if someone else solved this issue properly.

Continuing specific entry is not allowed because of Click's validation

Credit to @StanczakDominik

This allows toggl continue Presentation from the command line to work properly.

On my system, passing that in resulted in:

Error: Invalid value for "[DESCR]": Unknown Time entry under specification 'Presentation'!
While the Presentation entry is definitely found in existing entries (as checked via the -d flag). Without the click type check, it works nicely. I'm not sure if I broke any existing functionality with that change, but it hasn't crashed on me yet.

Back to the presentation! 😄

Continue last entry restarts the last entry

When the last (stopped) entry is "entry desc", executing the following:

toggl continue "entry desc" 

continues the previous, existing entry without changing its start time - rather than creating a new one with the same info and a start time of now. I would have expected that it is syntatic sugar for toggl start "entry desc" with the project auto-attached.

InsecurePlatformWarning error on osx

I had issues regarding the ssl certificate, when running the app:

ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This
prevents urllib3 from configuring SSL appropriately and may cause certain SSL 
connections to fail. For more information, see
https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.

fixed it installing the following:
pip install pyopenssl ndg-httpsclient pyasn1

In case it helps someone.

Default duration 1 hour + x

The stop function without parameters always adds 1 hour to the actual duration for no reason.
While the output is correct:
./toggl.sh -v stop
"general" stopped at 02:05PM and lasted for 17.0m48.0s
Toggle desktop and the website then say that the work has a duration of 1 hour plus 17 minutes.

I'm not sure if daylight saving time/summer time is part of the problem but using utcnow() solves the issue for me:

def now(self):
return self.tz.localize( datetime.datetime.now() )

def now(self):
return self.tz.localize( datetime.datetime.utcnow() )

Referencing time entries based on their order

For easier management of time entries, it would be better to employ special syntax, that would refer to time entry not with ID or its description, but by the order they are presented in the listing.

The listing would have numbered lines:

    Description  Duration                  Start                   Stop
 1. Some entry    0:04:09  9:07:32 AM 01/21/2019                running
 2. Some entry    0:00:11             8:54:57 AM  8:55:08 AM 01/21/2019
 3. Some entry    0:02:00             8:54:08 AM  8:56:08 AM 01/21/2019

Then it would be possible to reference the issues like: toggl continue $2, toggl rm $3.

Potential problems:

If there is a change in the state from another source (Toggl's web client, other tool using it API calls) the referenced lines may differ and hence you can modify (or worse - delete) lines that you have not intended.

One solution to this problem, could be to store the state of the last command locally and when a call with this syntax, the ID of the referenced line would have to be retrieved from the previously stored state. This could go hand in hand with #86. But even then it could resolve in problems.

Let me know what you think and if you would be interested in such a feature!

using the projects command returns an error

File "./toggl.py", line 811, in
sys.exit(main())
File "./toggl.py", line 792, in main
Logger.info( ProjectList() )
File "./toggl.py", line 286, in info
print msg+end,
TypeError: unsupported operand type(s) for +: 'ProjectList' and 'str'

auto created ~/.togglrc permissions

hi think this program should chmod 600 ~/.togglrc after it has created the file. it contains password, and user may forgot (or not aware of) that this file should be protected for unwanted eyes

Connect Tag model to TimeEntry model

If a TimeEntry currently has tags associated to it, they are stored as a generic SetField. To link the proper Tag model to the time entries the tags field of TimeEntry should probably change to a MappingField, but cardinality 'many' has not yet been implemented for those.

I'd like to look into it, but I'm stuck with what has to be done to implement that. Any pointers would be very helpful.

ntp and utf-8

If you localtime is behind the standard time,
you will get the following messages when you stop a task.

400 Client Error: Bad Request
Stop time must be after start time

I think use standard time instead of localtime would be better.
So I put ntp() function in the DateAndTime.
And I add utf-8 support also.
I hope this would be useful.

 toggl.py | 47 +++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 43 insertions(+), 4 deletions(-)

diff --git toggl.py toggl.py
index 03761df..de3a453 100755
--- toggl.py
+++ toggl.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
 """
 toggl.py

@@ -25,6 +26,10 @@ import requests
 import sys
 import time
 import urllib
+import socket
+import struct
+from socket import AF_INET, SOCK_DGRAM
+

 TOGGL_URL = "https://www.toggl.com/api/v8"
 VERBOSE = False # verbose output?
@@ -203,7 +208,11 @@ class DateAndTime(object):
         """
         Returns "now" as a localized datetime object.
         """
-        return self.tz.localize( datetime.datetime.now() ) 
+        t = self.ntp()
+        if t:
+            return self.tz.localize( datetime.datetime.fromtimestamp(t) )
+        else:
+            return self.tz.localize( datetime.datetime.now() )

     def parse_local_datetime_str(self, datetime_str):
         """
@@ -236,6 +245,36 @@ class DateAndTime(object):
             datetime.timedelta(days=1) # subtract one day from today at midnight
         )

+    def ntp(self):
+        """
+        Returns the POSIX timestamp.
+        """
+        # ref: http://blog.mattcrampton.com/post/88291892461/query-an-ntp-server-from-python
+        # TODO: behind a proxy
+        # TODO: custom ntp conf
+        host = "pool.ntp.org"
+        port = 123
+        buf = 1024
+        address = (host,port)
+        msg = '\x1b' + 47 * '\0'
+        
+        # reference time (in seconds since 1900-01-01 00:00:00)
+        TIME1970 = 2208988800L # 1970-01-01 00:00:00
+        
+        # connect to server
+        client = socket.socket(AF_INET, SOCK_DGRAM)
+        client.sendto(msg, address)
+        client.settimeout(2)
+        
+        for x in range(1, 3):
+            try:
+                msg, address = client.recvfrom( buf )
+            except:
+                pass
+        
+        t = struct.unpack("!12I", msg )[10]
+        return t - TIME1970 if t else 0
+
 #----------------------------------------------------------------------------
 # Logger 
 #----------------------------------------------------------------------------
@@ -764,7 +803,7 @@ class TimeEntryList(object):
         returned.
         """
         for entry in reversed(self.time_entries):
-            if entry.get('description') == description:
+            if entry.get('description') == description.decode('utf-8'):
                 return entry
         return None

@@ -829,7 +868,7 @@ class TimeEntryList(object):
             s += date + "\n"
             duration = 0
             for entry in days[date]:
-                s += str(entry) + "\n"
+                s += unicode(entry) + "\n"
                 duration += entry.normalized_duration()
             s += "  (%s)\n" % DateAndTime().elapsed_time(int(duration))
         return s.rstrip() # strip trailing \n
@@ -1124,7 +1163,7 @@ class CLI(object):
         entry = TimeEntryList().now()

         if entry != None:
-            Logger.info(str(entry))
+            Logger.info(unicode(entry))
         else:
             Logger.info("You're not working on anything right now.")

Project name with spaces?

toggl add test :MyWorkSpace @"My Awesome - Project" 'd'10:00 fails with a project not found RuntimeError.

API reference documentation

Currently, there is no detailed API wrapper's reference, where detailed signatures are explained for the wrapper classes.

There are few criteria from my side:

  • Needs to be generated from doc-strings (otherwise it will get stale very fast as people are lazy to update hand-written reference, me included).
  • Needs to be compatible with mkdocs (I am not a big fan of reST and Sphinx).

Currently, I am aware of pydoc-markdown, but the current version does not support documentation of class's attributes. There is a new version being developed which will support it but for now, I am not aware of any other solutions, so if you know about any please let me know!

self.args[0] = toggl.py/sh

def act(self) in toggle.py checks self.args[0] for strings such as 'ls'.

if len(self.args) == 0 or self.args[0] == "ls":
Logger.info(TimeEntryList())
elif self.args[0] == "add":
self._add_time_entry(self.args[1:])
.....

I had to change this to self.args[1] as self.args[0] is the program name (Linux).
Adding print(self.args[0]) to act(self) at line 1041 reveals this.
Is this a Windows/Linux problem?
I ended up simply replacing most occurrences of 'self.args[0]' with 'self.args[1]'.

toggl.py now reports error when working on a project

toggl.py now

work fine if not working on a project
when working on a project it does :

Traceback (most recent call last):
  File "toggl.py", line 1233, in <module>
    CLI().act()
  File "toggl.py", line 1015, in act
    self._list_current_time_entry()
  File "toggl.py", line 1177, in _list_current_time_entry
    Logger.info(str(entry))
  File "toggl.py", line 716, in __str__
    project_name = " @%s " % ProjectList().find_by_id(self.data['pid'])['name']
TypeError: 'NoneType' object has no attribute '__getitem__'

Caching

With the introduction of Django ORM like framework, it allows easy usage of the API wrapper classes, but it also introduces quiet bit overhead for fetching resources, which resolve in expensive API calls. Currently, for CLI usage, this should not be a problem, but if somebody would like to use the API wrappers in more intensive ways he could reach the API's throttling limitations quiet soon.

To address this problem, I would like to add support for caching results. This should be most probably on the low level of toggl.utils.other.toggl calls.

Toggl's API supports bulk fetching of data using https://www.toggl.com/api/v8/me?with_related_data=true, which could be used for population of the cache.
Also, the same endpoint supports since=<timestamp>, that fetches changed data, which could be used for preventing the cached data to become stale.

UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6'

Changing the timezone to Europe/Stockholm in .togglrc gives me this error when trying to run toggl.py:

Traceback (most recent call last):
  File "toggl.py", line 1063, in <module>
    CLI().act()
  File "toggl.py", line 893, in act
    Logger.info(TimeEntryList())
  File "toggl.py", line 271, in info
    print("%s%s" % (msg, end)),
  File "toggl.py", line 752, in __str__
    s += str(entry) + "\n"
UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' in position 6: ordinal not in range(128)

Timezones with continue and stop

My timezone is set for Vancouver and I'm currently in Mexico, 2 hours ahead. If I run continue, a new entry is started with the correct time. If I then run stop, the entry is stopped 2 hours ahead.

Continuation Across Days

Continuing an entry across days doesn't seem to work correctly. It assumes that the entry has been in progress since the original start time.

Python 3 Support

c:\git\toggl-cli>python --version
Python 3.4.3

c:\git\toggl-cli>python toggl.py
  File "toggl.py", line 928
    count=0
          ^
TabError: inconsistent use of tabs and spaces in indentation

ubuntu 18.04 LTS - installation not healthy

Clean 18.04 LTS image, for example digital ocean

python3 --version
Python 3.6.7

pip3 --version
pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)

pip3 install togglCli==2.0.2
...

root@ubuntu-s-1vcpu-1gb-ams3-01:~# toggl
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 442, in _get_version_from_pkg_resources
    provider = pkg_resources.get_provider(requirement)
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 344, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 892, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 778, in resolve
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'toggl' distribution was not found and is required by the application

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/toggl", line 7, in <module>
    from toggl.toggl import main
  File "/usr/local/lib/python3.6/dist-packages/toggl/__init__.py", line 3, in <module>
    VERSION = VersionInfo('toggl').semantic_version()
  File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 462, in semantic_version
    self._semantic = self._get_version_from_pkg_resources()
  File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 449, in _get_version_from_pkg_resources
    result_string = packaging.get_version(self.package)
  File "/usr/local/lib/python3.6/dist-packages/pbr/packaging.py", line 848, in get_version
    name=package_name))
Exception: Versioning for this project requires either an sdist tarball, or access to an upstream git repository. It's also possible that there is a mismatch between the package name in setup.cfg and the argument given to pbr.version.VersionInfo. Project name toggl was given, but was not able to be found.

Unicode

  • [ ]
$ python2.7 toggl.py ls
Traceback (most recent call last):
  File "toggl.py", line 1265, in <module>
    CLI().act()
  File "toggl.py", line 1039, in act
    Logger.info(TimeEntryList())
  File "toggl.py", line 294, in info
    print("%s%s" % (msg, end)),
  File "toggl.py", line 881, in __str__
    s += str(entry) + "\n"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 11-12: ordinal not in range(128)
  • [ ]
$ python2.7 toggl.py continue "Posição na rede"
toggl.py:808: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
  if entry.get('description') == description:
Traceback (most recent call last):
  File "toggl.py", line 1265, in <module>
    CLI().act()
  File "toggl.py", line 1045, in act
    self._continue_entry(self.args[1:])
  File "toggl.py", line 1102, in _continue_entry
    Logger.info("Did not find '%s' in list of entries." % args[0] )
IndexError: list index out of range

Improve duration argument format

Hi there! Cool tool you have created.

I have implemented pomodoro support, but had difficult time trying to understand duration argument format. My 10+ year of experience in bash/python was useless there, so maybe you can improve duration format in some way?

In docs we have:

add DESCR [:WORKSPACE] [@PROJECT] START_DATETIME ('d'DURATION | END_DATETIME)
[..]
add DESCR [:WORKSPACE] [@PROJECT] 'd'DURATION
[..]
DURATION = [[Hours:]Minutes:]Seconds

So I was like "wat?". I tried examples like:

* 'd'20min (at first I haven't noticed format left in last line of help, so used Toggl syntax like in their app)
* 'd'00:25:00
* 00:25:00
* and a couple of more.

So my suggestion would be:

  • Add examples section after actions section.
  • After "creates a completed time entry" write "see also: duration format"
  • Maybe add separate command for duration tasks entering, like "add-finished", "finish"

I think this can be improved in many ways, so my "suggestions" are not final in any way. Hope to discuss this with you in the future.

Errors when running unit tests

I get errors when running the unit tests:

E..............F.F....Removing toggl entries created by the test...
E
======================================================================
ERROR: test_iterator (__main__.TestClientList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 33, in test_iterator
    num_clients = len(self.list.client_list)
TypeError: object of type 'NoneType' has no len()

======================================================================
ERROR: tearDownModule (__main__)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 361, in tearDownModule
    if entry.get('description').startswith('unittest_'):
AttributeError: 'NoneType' object has no attribute 'startswith'

======================================================================
FAIL: test_start_simple (__main__.TestTimeEntry)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 220, in test_start_simple
    self.assertRaises(Exception, self.entry.start)
AssertionError: Exception not raised

======================================================================
FAIL: test_stop_simple (__main__.TestTimeEntry)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 248, in test_stop_simple
    self.assertRaises(Exception, self.entry.start)
AssertionError: Exception not raised

----------------------------------------------------------------------
Ran 22 tests in 52.641s

FAILED (failures=2, errors=2)

The test entries are also not deleted at the end. When running toggl.py ls I also only get the entries created today. Is this expected?

Stopping a continued entry

Sometimes botches the times:

$ t ls
Nov 03
email 56m42s

...

$ t continue email
email continued at 02:31PM
$ t stop
email stopped at 02:37PM
$ t ls
Nov 03
email 7h3m16s

ls throws error

The error:

./toggl.py ls
Traceback (most recent call last):
  File "./toggl.py", line 1265, in <module>
    CLI().act()
  File "./toggl.py", line 1039, in act
    Logger.info(TimeEntryList())
  File "./toggl.py", line 294, in info
    print("%s%s" % (msg, end)),
  File "./toggl.py", line 881, in __str__
    s += str(entry) + "\n"
  File "./toggl.py", line 740, in __str__
    project = ProjectList().find_by_id(self.data['pid'])
  File "./toggl.py", line 474, in find_by_id
    for project in self:
  File "/usr/lib/python2.7/dist-packages/six.py", line 558, in next
    return type(self).__next__(self)
  File "./toggl.py", line 499, in __next__
    if self.iter_index >= len(self.project_list):
TypeError: object of type 'NoneType' has no len()

IGNORE_START_TIMES not used

I don't think this configuration variable is being used. This should be read from the toggl API, since it's a global user setting stored in the user's profile.

Full(er) Unicode support and iCal generation - Patch included.

Hi there..

I have modified toggl.py a bit to better support outputting utf-8 and also to generate ical entries and being able to select a periode for which to get entries rather than just yesterday and today.

I am unsure how to get the patch uploaded properly. Do I need to download it via git and submit my changes that way? The formatting will probably get lost, but below is a dump of the patch:

--- ../toggl-cli-master/toggl.py    2015-09-24 17:46:44.000000000 +0200
+++ toggl.py    2015-10-16 10:05:55.629874565 +0200
@@ -269,7 +269,7 @@
         Prints msg if the current logging level >= INFO.
         """ 
         if Logger.level >= Logger.INFO:
-            print("%s%s" % (msg, end)),
+            print("%s%s" % (unicode(msg), unicode(end))),

 #----------------------------------------------------------------------------
 # toggl
@@ -486,8 +486,8 @@
             if 'cid' in project:
                for client in clients:
                    if project['cid'] == client['id']:
-                       client_name = " - %s" % client['name']
-            s = s + ":%s @%s%s\n" % (self.workspace['name'], project['name'], client_name)
+                       client_name = " - %s" % unicode(client['name'])
+            s = s + ":%s @%s%s\n" % (unicode(self.workspace['name']), unicode(project['name']), unicode(client_name))
         return s.rstrip() # strip trailing \n

 #----------------------------------------------------------------------------
@@ -542,6 +542,7 @@
             if project == None:
                 raise RuntimeError("Project '%s' not found." % project_name)
             self.data['pid'] = project['id']
+            self.data['project_name'] = project['name']

         if duration is not None:
             self.data['duration'] = duration
@@ -744,11 +745,11 @@

     __metaclass__ = Singleton

-    def __init__(self):
+    def __init__(self, start_time=DateAndTime().start_of_yesterday().isoformat('T'), stop_time=DateAndTime().last_minute_today().isoformat('T'), project_name=None):
         """
         Fetches time entry data from toggl.
         """
-        self.reload()
+        self.reload(start_time,stop_time)

     def __iter__(self):
         """
@@ -788,15 +789,15 @@
                 return entry
         return None

-    def reload(self):
+    #def reload(self,start_time,stop_time,project_name):
+    def reload(self,start_time,stop_time):
         """
         Force reloading time entry data from the server. Returns self for
         method chaining.
         """
         # Fetch time entries from 00:00:00 yesterday to 23:59:59 today.
         url = "%s/time_entries?start_date=%s&end_date=%s" % \
-            (TOGGL_URL, urllib.parse.quote(DateAndTime().start_of_yesterday().isoformat('T')), \
-            urllib.parse.quote(DateAndTime().last_minute_today().isoformat('T')))
+            (TOGGL_URL, urllib.parse.quote(start_time), urllib.parse.quote(stop_time))
         Logger.debug(url)
         entries = json.loads( toggl(url, 'get') )

@@ -830,12 +831,120 @@
             s += date + "\n"
             duration = 0
             for entry in days[date]:
-                s += str(entry) + "\n"
+                s += unicode(entry) + "\n"
                 duration += entry.normalized_duration()
             s += "  (%s)\n" % DateAndTime().elapsed_time(int(duration))
         return s.rstrip() # strip trailing \n

 #----------------------------------------------------------------------------
+# IcalEntryList
+#----------------------------------------------------------------------------
+class IcalEntryList(object):
+    """
+    A singleton list of recent TimeEntry objects.
+    """
+
+    __metaclass__ = Singleton
+
+    def __init__(self, start_time=DateAndTime().start_of_yesterday().isoformat('T'), stop_time=DateAndTime().last_minute_today().isoformat('T'), project_name=None):
+        """
+        Fetches time entry data from toggl.
+        """
+        self.reload(start_time,stop_time)
+        
+    def __iter__(self):
+        """
+        Start iterating over the time entries.
+        """
+        self.iter_index = 0
+        return self
+
+    def find_by_description(self, description):
+        """
+        Searches the list of entries for the one matching the given 
+        description, or return None. If more than one entry exists
+        with a matching description, the most recent one is
+        returned.
+        """
+        for entry in reversed(self.time_entries):
+            if entry.get('description') == description:
+                return entry
+        return None
+
+    def next(self):
+        """
+        Returns the next time entry object.
+        """
+        if self.iter_index >= len(self.time_entries):
+            raise StopIteration
+        else:
+            self.iter_index += 1
+            return self.time_entries[self.iter_index-1]
+    
+    def now(self):
+        """
+        Returns the current time entry object or None.
+        """
+        for entry in self:
+            if int(entry.get('duration')) < 0:
+                return entry
+        return None
+
+    #def reload(self,start_time,stop_time,project_name):
+    def reload(self,start_time,stop_time):
+        """
+        Force reloading time entry data from the server. Returns self for
+        method chaining.
+        """
+        # Fetch time entries from 00:00:00 yesterday to 23:59:59 today.
+        url = "%s/time_entries?start_date=%s&end_date=%s" % \
+            (TOGGL_URL, urllib.parse.quote(start_time), urllib.parse.quote(stop_time))
+        Logger.debug(url)
+        entries = json.loads( toggl(url, 'get') )
+        
+        # Build a list of entries.
+        self.time_entries = []
+        for entry in entries:
+            te = TimeEntry(data_dict=entry)
+            Logger.debug(te.json())
+            Logger.debug('---')
+            self.time_entries.append(te)
+
+        # Sort the list by start time.
+        sorted(self.time_entries, key=lambda entry: entry.data['start'])
+        return self
+
+    def __str__(self):
+        """
+        Returns a human-friendly list of recent time entries.
+        """
+        # Sort the time entries into buckets based on "Month Day" of the entry.
+        days = { }
+        for entry in self.time_entries:
+            start_time = DateAndTime().parse_iso_str(entry.get('start')).strftime("%Y-%m-%d")
+            if start_time not in days:
+                days[start_time] = []
+            days[start_time].append(entry)
+
+        # For each day, create calendar entries for each toggle entry.
+        s="BEGIN:VCALENDAR\nX-WR-CALNAME:Toggl Entries\nVERSION:2.0:CALSCALE:GREGORIAN\nMETHOD:PUBLISH\n"
+   count=0
+        for date in sorted(days.keys()):
+            for entry in days[date]:
+       #print vars(entry) + "\n"
+                s += "BEGIN:VEVENT\nDTSTART:%sZ\n" % entry.get('start')
+                s += "DTEND:%sZ\n" % entry.get('stop')
+       if entry.has('pid') == True:
+                    s += "SUMMARY:%s\nDESCRIPTION:%s\nSEQUENCE:%i\nEND:VEVENT\n" % (unicode(ProjectList().find_by_id(entry.data['pid'])['name']), unicode(entry.get('description')), count)
+       else:
+                    s += "SUMMARY:%s\nDESCRIPTION:%s\nSEQUENCE:%i\nEND:VEVENT\n" % (unicode(entry.get('description')), unicode(entry.get('description')), count)
+                count += 1
+                #duration += entry.normalized_duration()
+            #s += "  (%s)\n" % DateAndTime().elapsed_time(int(duration))
+        s += "END:VCALENDAR\n"
+        return s.rstrip() # strip trailing \n
+    
+#----------------------------------------------------------------------------
 # User
 #----------------------------------------------------------------------------
 class User(object):
@@ -898,7 +1007,8 @@
             "  add DESCR [:WORKSPACE] [@PROJECT] START_DATETIME ('d'DURATION | END_DATETIME)\n\tcreates a completed time entry\n"
             "  clients\n\tlists all clients\n"
             "  continue DESCR\n\trestarts the given entry\n"
-            "  ls\n\tlist recent time entries\n"
+            "  ls [starttime endtime]\n\tlist (recent) time entries\n"
+            "  ical [starttime endtime]\n\tdump iCal list of (recent) time entries\n"
             "  now\n\tprint what you're working on now\n"
             "  workspaces\n\tlists all workspaces\n"
             "  projects [:WORKSPACE]\n\tlists all projects\n"
@@ -907,7 +1017,9 @@
             "  stop [DATETIME]\n\tstops the current entry\n"
             "  www\n\tvisits toggl.com\n"
             "\n"
-            "  DURATION = [[Hours:]Minutes:]Seconds\n")
+            "  DURATION = [[Hours:]Minutes:]Seconds\n"
+            "  starttime/endtime = YYYY-MM-DDThh:mm:ss+TZ:00\n"
+            "  e.g. starttime = 2015-10-15T00:00:00+02:00\n")
         self.parser.add_option("-q", "--quiet",
                               action="store_true", dest="quiet", default=False,
                               help="don't print anything")
@@ -979,8 +1091,14 @@
         """
         Performs the actions described by the list of arguments in self.args.
         """
-        if len(self.args) == 0 or self.args[0] == "ls":
+        if len(self.args) == 3 and self.args[0] == "ls":
+            Logger.info(TimeEntryList(self.args[1],self.args[2]))
+        elif len(self.args) == 0 or self.args[0] == "ls":
             Logger.info(TimeEntryList())
+        elif len(self.args) == 1 and self.args[0] == "ical":
+            Logger.info(IcalEntryList())
+        elif len(self.args) == 3 or self.args[0] == "ical":
+            Logger.info(IcalEntryList(self.args[1],self.args[2]))
         elif self.args[0] == "add":
             self._add_time_entry(self.args[1:])
         elif self.args[0] == "clients":
@@ -1006,7 +1124,7 @@

     def _show_projects(self, args):
         workspace_name = self._get_workspace_arg(args, optional=True)
-        print(ProjectList(workspace_name))
+        print(unicode(ProjectList(workspace_name)))

     def _continue_entry(self, args):
         """

'TimeEntry.created_with' is write-only

Field created_with on TimeEntry entity is only write-only, in a sense of the field is not part of fields returned from Toggl API when querying for list/detail operation.

This field serves Toggl developers to identify the creators of entries that are problematic to contact them.

default workspace

Hi, I'm new to toggl.py. Thanks for writing such a great tool!

From reading the usage output of toggl.py -h I thought that that the [:WORKSPACE] argument would be optional. However without that argument I get the following:

$ toggl.py add "pmwg weekly" @meetings 06:30am 07:30am
Traceback (most recent call last):
  File "/Users/mturquette/.local/bin/toggl.py", line 1175, in <module>
    CLI().act()
  File "/Users/mturquette/.local/bin/toggl.py", line 981, in act
    self._add_time_entry(self.args[1:])
  File "/Users/mturquette/.local/bin/toggl.py", line 948, in _add_time_entry
    project = ProjectList(workspace["name"]).find_by_name(project_name)
UnboundLocalError: local variable 'workspace' referenced before assignment

By specifying my one and only workspace I can make the command succeed:

$ toggl.py add "pmwg weekly" ":Mike Turquette's workspace" @meetings 06:30am 07:30am
pmwg weekly added

I can always write a bash function which wraps toggl.py and does this stuff for me, but I would rather that ~/.togglrc specify a "default workspace" so that I don't have to do that.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.